Skip loading admin pages on frontend/cron requests#335
Conversation
Admin pages that only register admin menus, forms, and wp_ajax_ handlers are now gated behind is_admin() || wp_doing_ajax(). Four classes with frontend hooks (admin bar, nav menu filters, SSO magic links) continue to load on all requests. Also moves the event badge count query from init to admin_init so it no longer runs on frontend or cron requests. Saves ~100-150ms per non-admin request based on Xdebug profiling. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds an admin/AJAX-only admin pages loader, gates several admin/script initializations and Current initialization to admin/AJAX contexts, switches an event-list badge hook to Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
🔨 Build Complete - Ready for Testing!📦 Download Build Artifact (Recommended)Download the zip build, upload to WordPress and test:
🌐 Test in WordPress Playground (Very Experimental)Click the link below to instantly test this PR in your browser - no installation needed! Login credentials: |
- Replace metadata_exists() + get_metadata() double lookup with single get_metadata_raw() call in Base_Model::get_meta() - Guard Block_Editor_Widget_Manager::register_scripts with is_admin() to skip frontend script registration - Skip redundant load_currents() re-run on admin/AJAX wp hook - Add unit tests for Block_Editor_Widget_Manager and Current class - Configure code coverage: fix phpunit.xml.dist, update test:coverage npm script, install xdebug in CI, add codecov.yml with patch/project thresholds Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create Manager_Test_Trait with shared assertions for singleton behavior, slug, and model_class properties - Add tests for 15 previously untested managers: Block, Broadcast, Cache, Checkout_Form, Customer, Discount_Code, Event, Form, Job, Limitation, Notes, Notification, Product, Rating_Notice, Signup_Fields, Site, Visits, Webhook - Customer_Manager: test login tracking, email verification, heartbeat - Site_Manager: test hyphen validation, search/replace, login URLs - Event_Manager: test event registration, dispatch, model events - Form_Manager: test form registration, lookup, URL generation - Limitation_Manager: test object type detection - Signup_Fields_Manager: test field type accessors - Load test traits from bootstrap.php 105 new tests, 554 total (up from 449). All passing. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🔨 Build Complete - Ready for Testing!📦 Download Build Artifact (Recommended)Download the zip build, upload to WordPress and test:
🌐 Test in WordPress Playground (Very Experimental)Click the link below to instantly test this PR in your browser - no installation needed! Login credentials: |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In @.github/workflows/tests.yml:
- Around line 88-93: The workflow uses codecov/codecov-action@v5 with the
deprecated input name "file"; update the action input from "file:
./coverage.xml" to the new "files: ./coverage.xml" (preserving the same path) so
the Codecov action recognizes the coverage artifact; locate the
codecov/codecov-action@v5 block in the YAML and replace the "file" key with
"files" (leave other keys like flags, name, and fail_ci_if_error unchanged).
In `@phpunit.xml.dist`:
- Line 2: The PHPUnit config removed the error-to-exception attributes
(convertErrorsToExceptions, convertNoticesToExceptions,
convertWarningsToExceptions) which changes test behavior; either restore these
attributes on the <phpunit> element in phpunit.xml.dist to re-enable converting
PHP errors/notices/warnings to exceptions, or if intentional, add a clear
justification in the commit message and repository changelog stating why
conversion was disabled and the implications for test coverage and error
detection; specifically update the <phpunit ...> element to include
convertErrorsToExceptions="true" convertNoticesToExceptions="true"
convertWarningsToExceptions="true" or provide the explicit commit message
justification.
In `@tests/WP_Ultimo/Builders/Block_Editor/Block_Editor_Widget_Manager_Test.php`:
- Around line 97-108: The test method
test_is_block_preview_returns_true_in_rest_edit_context uses
define('REST_REQUEST', true) which can fail or cause flakiness if REST_REQUEST
is already defined; update the test to avoid blind redefinition by checking
defined('REST_REQUEST') before defining (e.g., if (!defined('REST_REQUEST'))
define('REST_REQUEST', true)), or better annotate the test with
`@runInSeparateProcess` to isolate constant state, and ensure you still set/unset
$_GET['context'] around the call to $this->manager->is_block_preview(false) so
the behavior is deterministic; reference the test method name
test_is_block_preview_returns_true_in_rest_edit_context and the production
method is_block_preview on $this->manager when making the change.
In `@tests/WP_Ultimo/Managers/Cache_Manager_Test.php`:
- Around line 12-26: Add minimal PHPDoc docblocks for the class
Cache_Manager_Test and its protected helper methods get_manager_class,
get_expected_slug, and get_expected_model_class to satisfy code-quality checks;
insert a short class docblock above "class Cache_Manager_Test" describing the
test class, and add brief "@return" annotations for each protected method (e.g.,
"@return string" or "@return string|null" as appropriate) directly above their
declarations to document return types and purpose.
In `@tests/WP_Ultimo/Managers/Event_Manager_Test.php`:
- Around line 117-118: The local variable $manager is assigned but never used;
remove the unused assignment to satisfy PHPMD by deleting the line that sets
$manager = $this->get_manager_instance(); (if you intended only to invoke
get_manager_instance() for side effects, instead call
$this->get_manager_instance(); without assigning to $manager). Reference
symbols: $manager and get_manager_instance().
In `@tests/WP_Ultimo/Managers/Manager_Test_Trait.php`:
- Line 1: Rename the file currently named "Manager_Test_Trait.php" to use the
project's kebab-case convention: "manager-test-trait.php"; ensure any references
or includes that import Manager_Test_Trait (e.g., require/include, autoloader
mappings or test bootstrapping) are updated to the new filename so the trait
continues to load correctly.
In `@tests/WP_Ultimo/Managers/Rating_Notice_Manager_Test.php`:
- Around line 12-25: Add minimal PHPDoc blocks for the
Rating_Notice_Manager_Test class and its protected methods get_manager_class,
get_expected_slug, and get_expected_model_class to satisfy code-quality checks;
for the class include a one-line description (e.g., "Unit tests for
Rating_Notice_Manager") and for each protected method add a short description
and `@return` tag matching their signatures (string for get_manager_class and
?string for the other two) so the docblocks document purpose and return types.
In `@tests/WP_Ultimo/Managers/Webhook_Manager_Test.php`:
- Line 12: Add a minimal PHPDoc class docblock immediately above the
Webhook_Manager_Test class declaration to satisfy code-quality checks; include
at least a short description and the `@covers` or `@group` tags if appropriate for
tests (place the docblock directly above "class Webhook_Manager_Test extends
\WP_UnitTestCase" so the linter recognizes it).
🧹 Nitpick comments (6)
package.json (1)
51-51: Cross-platform consideration for the Xdebug extension path.The script uses
xdebug.sowhich is Linux/macOS-specific. On Windows, the extension would bexdebug.dll. This should work fine for CI (typically Ubuntu-based) but may require adjustment for local development on Windows.If cross-platform local development is a concern, consider documenting this limitation or using a shell script that detects the OS.
tests/WP_Ultimo/Managers/Manager_Test_Trait.php (1)
99-112: Consider usingmarkTestSkipped()for clarity.Using
$this->assertTrue(true, ...)to indicate "not applicable" works but is semantically unclear. PHPUnit's$this->markTestSkipped()provides clearer intent and better test reporting.Proposed improvement
public function test_slug_property(): void { $expected = $this->get_expected_slug(); if (null === $expected) { - $this->assertTrue(true, 'Manager does not define a slug.'); + $this->markTestSkipped('Manager does not define a slug.'); return; }Apply the same pattern to
test_model_class_property().tests/WP_Ultimo/Managers/Event_Manager_Test.php (1)
96-106: Clean up actions to prevent cross-test leakage.The anonymous actions remain registered after the test, which can affect later tests that fire
wu_event. Consider removing them after assertions.🧪 Suggested cleanup
- add_action('wu_event', function () use (&$generic_fired) { + $generic_cb = function () use (&$generic_fired) { $generic_fired = true; - }); + }; + add_action('wu_event', $generic_cb); - add_action('wu_event_test_fire', function () use (&$specific_fired) { + $specific_cb = function () use (&$specific_fired) { $specific_fired = true; - }); + }; + add_action('wu_event_test_fire', $specific_cb); @@ $manager->do_event('test_fire', ['sample' => 'data']); $this->assertTrue($generic_fired, 'wu_event action should have fired.'); $this->assertTrue($specific_fired, 'wu_event_test_fire action should have fired.'); + + remove_action('wu_event', $generic_cb); + remove_action('wu_event_test_fire', $specific_cb);tests/WP_Ultimo/Managers/Customer_Manager_Test.php (1)
57-65: Consider using$this->expectNotToPerformAssertions()or adding meaningful assertions.Using
$this->assertTrue(true)is a weak pattern that only proves no exception was thrown. For clearer intent, either:
- Call
$this->expectNotToPerformAssertions()at the start of the test if you genuinely only want to verify no exception occurs.- Add a meaningful assertion (e.g., verify method returns
nullorvoid).This same pattern appears at lines 77 and 103.
💡 Suggested improvement
public function test_log_ip_and_last_login_with_nonexistent_user(): void { + $this->expectNotToPerformAssertions(); $manager = $this->get_manager_instance(); // Should not throw — just return early. $manager->log_ip_and_last_login('nonexistent_user_xyz', null); - - $this->assertTrue(true); }tests/WP_Ultimo/Managers/Site_Manager_Test.php (2)
81-86: Fix array double arrow alignment.Static analysis flagged inconsistent spacing around
=>operators.🔧 Proposed fix
$input = [ - 'good_key' => 'value1', - '' => 'value2', - 'another' => 'value3', - false => 'value4', + 'good_key' => 'value1', + '' => 'value2', + 'another' => 'value3', + false => 'value4', ];
141-156: Consider usingsetUp/tearDownfor user context management.The test manually restores user context with
wp_set_current_user(0). For better isolation and to prevent test pollution if the test fails early, consider using class-level setup/teardown.💡 Alternative approach
private int $original_user_id; public function set_up(): void { parent::set_up(); $this->original_user_id = get_current_user_id(); } public function tear_down(): void { wp_set_current_user($this->original_user_id); parent::tear_down(); }
| uses: codecov/codecov-action@v5 | ||
| with: | ||
| file: ./coverage.xml | ||
| flags: unittests | ||
| name: codecov-umbrella | ||
| name: php-${{ matrix.php-version }} | ||
| fail_ci_if_error: false |
There was a problem hiding this comment.
Fix: file input renamed to files in codecov-action@v5.
The static analysis correctly identified that codecov/codecov-action@v5 renamed the file input to files.
🐛 Proposed fix
- name: Upload coverage to Codecov
if: matrix.php-version == '8.3'
uses: codecov/codecov-action@v5
with:
- file: ./coverage.xml
+ files: ./coverage.xml
flags: unittests
name: php-${{ matrix.php-version }}
fail_ci_if_error: false📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| uses: codecov/codecov-action@v5 | |
| with: | |
| file: ./coverage.xml | |
| flags: unittests | |
| name: codecov-umbrella | |
| name: php-${{ matrix.php-version }} | |
| fail_ci_if_error: false | |
| uses: codecov/codecov-action@v5 | |
| with: | |
| files: ./coverage.xml | |
| flags: unittests | |
| name: php-${{ matrix.php-version }} | |
| fail_ci_if_error: false |
🧰 Tools
🪛 actionlint (1.7.10)
[error] 90-90: input "file" is not defined in action "codecov/codecov-action@v5". available inputs are "base_sha", "binary", "codecov_yml_path", "commit_parent", "directory", "disable_file_fixes", "disable_safe_directory", "disable_search", "disable_telem", "dry_run", "env_vars", "exclude", "fail_ci_if_error", "files", "flags", "force", "gcov_args", "gcov_executable", "gcov_ignore", "gcov_include", "git_service", "handle_no_reports_found", "job_code", "name", "network_filter", "network_prefix", "os", "override_branch", "override_build", "override_build_url", "override_commit", "override_pr", "plugins", "recurse_submodules", "report_code", "report_type", "root_dir", "run_command", "skip_validation", "slug", "swift_project", "token", "url", "use_legacy_upload_endpoint", "use_oidc", "use_pypi", "verbose", "version", "working-directory"
(action)
🤖 Prompt for AI Agents
In @.github/workflows/tests.yml around lines 88 - 93, The workflow uses
codecov/codecov-action@v5 with the deprecated input name "file"; update the
action input from "file: ./coverage.xml" to the new "files: ./coverage.xml"
(preserving the same path) so the Codecov action recognizes the coverage
artifact; locate the codecov/codecov-action@v5 block in the YAML and replace the
"file" key with "files" (leave other keys like flags, name, and fail_ci_if_error
unchanged).
| @@ -1,5 +1,5 @@ | |||
| <?xml version="1.0"?> | |||
| <phpunit bootstrap="tests/bootstrap.php" backupGlobals="false" colors="true" convertErrorsToExceptions="true" convertNoticesToExceptions="true" convertWarningsToExceptions="true"> | |||
| <phpunit bootstrap="tests/bootstrap.php" backupGlobals="false" colors="true"> | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat composer.json | jq '.["require-dev"]["phpunit/phpunit"] // .require["phpunit/phpunit"]'Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 90
🌐 Web query:
PHPUnit convertErrorsToExceptions convertNoticesToExceptions convertWarningsToExceptions deprecated removed version
💡 Result:
-
convertErrorsToExceptions,convertNoticesToExceptions, andconvertWarningsToExceptionsare PHPUnit 9.x-era<phpunit>XML attributes (documented in the PHPUnit 9.6 manual). They control whether PHPUnit’s error handler converts PHP errors/notices/warnings into exceptions that error the test. [1] -
These
convert*ToExceptionsXML attributes were removed for PHPUnit 10.0 as part of the error-handling redesign (planned/tracked for the PHPUnit 10.0 milestone). [2] The PHPUnit 10.x XML configuration reference no longer lists them; instead you control behavior using options such asstopOnNotice,stopOnWarning,stopOnDeprecation, andfailOnNotice/failOnWarning/failOnDeprecation. [3]
Sources:
[1] PHPUnit 9.6 XML configuration docs (lists convertErrorsToExceptions, convertNoticesToExceptions, convertWarningsToExceptions) (docs.phpunit.de)
[2] PHPUnit issue/milestone for removing these options in PHPUnit 10.0 (github.com)
[3] PHPUnit 10.5 XML configuration docs (shows stopOn* / failOn*, and absence of convert*ToExceptions) (docs.phpunit.de)
Citations:
- 1: https://docs.phpunit.de/en/9.6/configuration.html?utm_source=openai
- 2: Mark tests with deprecations, notices, or warnings as risky (instead of errored) sebastianbergmann/phpunit#4784
- 3: https://docs.phpunit.de/en/10.5/configuration.html
Removing PHPUnit 9.x error-handling attributes requires justification.
The project uses PHPUnit ^9.6.22, which supports convertErrorsToExceptions, convertNoticesToExceptions, and convertWarningsToExceptions. Removing these attributes changes test behavior—PHP notices and warnings will no longer be converted to exceptions, potentially masking issues during testing. If this is intentional, clarify the reasoning in the commit message.
🤖 Prompt for AI Agents
In `@phpunit.xml.dist` at line 2, The PHPUnit config removed the
error-to-exception attributes (convertErrorsToExceptions,
convertNoticesToExceptions, convertWarningsToExceptions) which changes test
behavior; either restore these attributes on the <phpunit> element in
phpunit.xml.dist to re-enable converting PHP errors/notices/warnings to
exceptions, or if intentional, add a clear justification in the commit message
and repository changelog stating why conversion was disabled and the
implications for test coverage and error detection; specifically update the
<phpunit ...> element to include convertErrorsToExceptions="true"
convertNoticesToExceptions="true" convertWarningsToExceptions="true" or provide
the explicit commit message justification.
| public function test_is_block_preview_returns_true_in_rest_edit_context(): void { | ||
|
|
||
| define('REST_REQUEST', true) || true; | ||
|
|
||
| $_GET['context'] = 'edit'; | ||
|
|
||
| $result = $this->manager->is_block_preview(false); | ||
|
|
||
| $this->assertTrue($result, 'Should return true when in REST edit context.'); | ||
|
|
||
| unset($_GET['context']); | ||
| } |
There was a problem hiding this comment.
Constant redefinition issue will cause test failures.
define('REST_REQUEST', true) will fail silently if the constant is already defined (by WordPress core or other tests), and the || true is a no-op that doesn't help. This can lead to flaky tests.
Consider using runkit or refactoring to avoid constant-based detection, or at minimum check if the constant is already defined:
Proposed fix
- define('REST_REQUEST', true) || true;
+ if ( ! defined('REST_REQUEST') ) {
+ define('REST_REQUEST', true);
+ }Note: If REST_REQUEST is already defined as false from a previous test context, this test will still fail. You may need test isolation via @runInSeparateProcess annotation or restructure to avoid relying on constants.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public function test_is_block_preview_returns_true_in_rest_edit_context(): void { | |
| define('REST_REQUEST', true) || true; | |
| $_GET['context'] = 'edit'; | |
| $result = $this->manager->is_block_preview(false); | |
| $this->assertTrue($result, 'Should return true when in REST edit context.'); | |
| unset($_GET['context']); | |
| } | |
| public function test_is_block_preview_returns_true_in_rest_edit_context(): void { | |
| if ( ! defined('REST_REQUEST') ) { | |
| define('REST_REQUEST', true); | |
| } | |
| $_GET['context'] = 'edit'; | |
| $result = $this->manager->is_block_preview(false); | |
| $this->assertTrue($result, 'Should return true when in REST edit context.'); | |
| unset($_GET['context']); | |
| } |
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Builders/Block_Editor/Block_Editor_Widget_Manager_Test.php`
around lines 97 - 108, The test method
test_is_block_preview_returns_true_in_rest_edit_context uses
define('REST_REQUEST', true) which can fail or cause flakiness if REST_REQUEST
is already defined; update the test to avoid blind redefinition by checking
defined('REST_REQUEST') before defining (e.g., if (!defined('REST_REQUEST'))
define('REST_REQUEST', true)), or better annotate the test with
`@runInSeparateProcess` to isolate constant state, and ensure you still set/unset
$_GET['context'] around the call to $this->manager->is_block_preview(false) so
the behavior is deterministic; reference the test method name
test_is_block_preview_returns_true_in_rest_edit_context and the production
method is_block_preview on $this->manager when making the change.
| class Cache_Manager_Test extends \WP_UnitTestCase { | ||
|
|
||
| use Manager_Test_Trait; | ||
|
|
||
| protected function get_manager_class(): string { | ||
| return Cache_Manager::class; | ||
| } | ||
|
|
||
| protected function get_expected_slug(): ?string { | ||
| return null; | ||
| } | ||
|
|
||
| protected function get_expected_model_class(): ?string { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Add missing docblocks for class and protected helpers (Lines 12–26).
Code-quality checks report missing doc comments for the class and its protected helper methods. Add minimal docblocks to clear the failure.
✅ Suggested fix
-class Cache_Manager_Test extends \WP_UnitTestCase {
+/**
+ * Tests for Cache_Manager.
+ */
+class Cache_Manager_Test extends \WP_UnitTestCase {
use Manager_Test_Trait;
+ /**
+ * `@return` string
+ */
protected function get_manager_class(): string {
return Cache_Manager::class;
}
+ /**
+ * `@return` string|null
+ */
protected function get_expected_slug(): ?string {
return null;
}
+ /**
+ * `@return` string|null
+ */
protected function get_expected_model_class(): ?string {
return null;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| class Cache_Manager_Test extends \WP_UnitTestCase { | |
| use Manager_Test_Trait; | |
| protected function get_manager_class(): string { | |
| return Cache_Manager::class; | |
| } | |
| protected function get_expected_slug(): ?string { | |
| return null; | |
| } | |
| protected function get_expected_model_class(): ?string { | |
| return null; | |
| } | |
| /** | |
| * Tests for Cache_Manager. | |
| */ | |
| class Cache_Manager_Test extends \WP_UnitTestCase { | |
| use Manager_Test_Trait; | |
| /** | |
| * `@return` string | |
| */ | |
| protected function get_manager_class(): string { | |
| return Cache_Manager::class; | |
| } | |
| /** | |
| * `@return` string|null | |
| */ | |
| protected function get_expected_slug(): ?string { | |
| return null; | |
| } | |
| /** | |
| * `@return` string|null | |
| */ | |
| protected function get_expected_model_class(): ?string { | |
| return null; | |
| } |
🧰 Tools
🪛 GitHub Check: Code Quality Checks
[failure] 24-24:
Missing doc comment for function get_expected_model_class()
[failure] 20-20:
Missing doc comment for function get_expected_slug()
[failure] 16-16:
Missing doc comment for function get_manager_class()
[failure] 12-12:
Missing doc comment for class Cache_Manager_Test
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Managers/Cache_Manager_Test.php` around lines 12 - 26, Add
minimal PHPDoc docblocks for the class Cache_Manager_Test and its protected
helper methods get_manager_class, get_expected_slug, and
get_expected_model_class to satisfy code-quality checks; insert a short class
docblock above "class Cache_Manager_Test" describing the test class, and add
brief "@return" annotations for each protected method (e.g., "@return string" or
"@return string|null" as appropriate) directly above their declarations to
document return types and purpose.
| $manager = $this->get_manager_instance(); | ||
|
|
There was a problem hiding this comment.
Remove unused $manager to satisfy PHPMD.
This variable is never used and triggers the code-quality check.
🧹 Suggested fix
- $manager = $this->get_manager_instance();
-
$event = new \WP_Ultimo\Models\Event(📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $manager = $this->get_manager_instance(); | |
| $event = new \WP_Ultimo\Models\Event( |
🧰 Tools
🪛 PHPMD (2.15.0)
[warning] 117-117: Avoid unused local variables such as '$manager'. (undefined)
(UnusedLocalVariable)
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Managers/Event_Manager_Test.php` around lines 117 - 118, The
local variable $manager is assigned but never used; remove the unused assignment
to satisfy PHPMD by deleting the line that sets $manager =
$this->get_manager_instance(); (if you intended only to invoke
get_manager_instance() for side effects, instead call
$this->get_manager_instance(); without assigning to $manager). Reference
symbols: $manager and get_manager_instance().
| @@ -0,0 +1,131 @@ | |||
| <?php | |||
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find all trait files and their naming patterns
fd -e php -t f . | xargs grep -l "^trait " | sortRepository: Ultimate-Multisite/ultimate-multisite
Length of output: 730
Rename file to follow project naming convention: manager-test-trait.php
All trait files in this project consistently use kebab-case format (e.g., trait-mcp-abilities.php, trait-rest-api.php, trait-singleton.php). This file should follow the same pattern: manager-test-trait.php.
🧰 Tools
🪛 GitHub Check: Code Quality Checks
[failure] 1-1:
Filenames should be all lowercase with hyphens as word separators. Expected manager-test-trait.php, but found Manager_Test_Trait.php.
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Managers/Manager_Test_Trait.php` at line 1, Rename the file
currently named "Manager_Test_Trait.php" to use the project's kebab-case
convention: "manager-test-trait.php"; ensure any references or includes that
import Manager_Test_Trait (e.g., require/include, autoloader mappings or test
bootstrapping) are updated to the new filename so the trait continues to load
correctly.
| class Rating_Notice_Manager_Test extends \WP_UnitTestCase { | ||
|
|
||
| use Manager_Test_Trait; | ||
|
|
||
| protected function get_manager_class(): string { | ||
| return Rating_Notice_Manager::class; | ||
| } | ||
|
|
||
| protected function get_expected_slug(): ?string { | ||
| return null; | ||
| } | ||
|
|
||
| protected function get_expected_model_class(): ?string { | ||
| return null; |
There was a problem hiding this comment.
Add PHPDoc blocks to satisfy code-quality checks.
The code quality checks report missing doc comments for the class and its protected methods, which can fail CI. Please add minimal PHPDoc to these elements.
📝 Suggested docblocks
-class Rating_Notice_Manager_Test extends \WP_UnitTestCase {
+/**
+ * `@covers` \WP_Ultimo\Managers\Rating_Notice_Manager
+ */
+class Rating_Notice_Manager_Test extends \WP_UnitTestCase {
@@
- protected function get_manager_class(): string {
+ /**
+ * `@return` class-string
+ */
+ protected function get_manager_class(): string {
return Rating_Notice_Manager::class;
}
@@
- protected function get_expected_slug(): ?string {
+ /**
+ * `@return` string|null
+ */
+ protected function get_expected_slug(): ?string {
return null;
}
@@
- protected function get_expected_model_class(): ?string {
+ /**
+ * `@return` string|null
+ */
+ protected function get_expected_model_class(): ?string {
return null;
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| class Rating_Notice_Manager_Test extends \WP_UnitTestCase { | |
| use Manager_Test_Trait; | |
| protected function get_manager_class(): string { | |
| return Rating_Notice_Manager::class; | |
| } | |
| protected function get_expected_slug(): ?string { | |
| return null; | |
| } | |
| protected function get_expected_model_class(): ?string { | |
| return null; | |
| /** | |
| * `@covers` \WP_Ultimo\Managers\Rating_Notice_Manager | |
| */ | |
| class Rating_Notice_Manager_Test extends \WP_UnitTestCase { | |
| use Manager_Test_Trait; | |
| /** | |
| * `@return` class-string | |
| */ | |
| protected function get_manager_class(): string { | |
| return Rating_Notice_Manager::class; | |
| } | |
| /** | |
| * `@return` string|null | |
| */ | |
| protected function get_expected_slug(): ?string { | |
| return null; | |
| } | |
| /** | |
| * `@return` string|null | |
| */ | |
| protected function get_expected_model_class(): ?string { | |
| return null; |
🧰 Tools
🪛 GitHub Check: Code Quality Checks
[failure] 24-24:
Missing doc comment for function get_expected_model_class()
[failure] 20-20:
Missing doc comment for function get_expected_slug()
[failure] 16-16:
Missing doc comment for function get_manager_class()
[failure] 12-12:
Missing doc comment for class Rating_Notice_Manager_Test
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Managers/Rating_Notice_Manager_Test.php` around lines 12 -
25, Add minimal PHPDoc blocks for the Rating_Notice_Manager_Test class and its
protected methods get_manager_class, get_expected_slug, and
get_expected_model_class to satisfy code-quality checks; for the class include a
one-line description (e.g., "Unit tests for Rating_Notice_Manager") and for each
protected method add a short description and `@return` tag matching their
signatures (string for get_manager_class and ?string for the other two) so the
docblocks document purpose and return types.
|
|
||
| use WP_Ultimo\Managers\Webhook_Manager; | ||
|
|
||
| class Webhook_Manager_Test extends \WP_UnitTestCase { |
There was a problem hiding this comment.
Add a class docblock to satisfy code-quality checks (Line 12).
CI reports a missing class doc comment. Add a minimal docblock above the class.
✅ Suggested fix
-class Webhook_Manager_Test extends \WP_UnitTestCase {
+/**
+ * Tests for Webhook_Manager.
+ */
+class Webhook_Manager_Test extends \WP_UnitTestCase {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| class Webhook_Manager_Test extends \WP_UnitTestCase { | |
| /** | |
| * Tests for Webhook_Manager. | |
| */ | |
| class Webhook_Manager_Test extends \WP_UnitTestCase { |
🧰 Tools
🪛 GitHub Check: Code Quality Checks
[failure] 12-12:
Missing doc comment for class Webhook_Manager_Test
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Managers/Webhook_Manager_Test.php` at line 12, Add a minimal
PHPDoc class docblock immediately above the Webhook_Manager_Test class
declaration to satisfy code-quality checks; include at least a short description
and the `@covers` or `@group` tags if appropriate for tests (place the docblock
directly above "class Webhook_Manager_Test extends \WP_UnitTestCase" so the
linter recognizes it).
The data-clipboard-text attribute was being double-escaped through esc_attr() in the edit template, breaking ClipboardJS. Changed attrs to use array format with proper per-value escaping. Also improved the clipboard JS init with copy feedback, fallback handling, and duplicate handler prevention. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🔨 Build Complete - Ready for Testing!📦 Download Build Artifact (Recommended)Download the zip build, upload to WordPress and test:
🌐 Test in WordPress Playground (Very Experimental)Click the link below to instantly test this PR in your browser - no installation needed! Login credentials: |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@assets/js/functions.js`:
- Around line 143-198: The init function wu_initialize_clipboardjs must remove
any prior fallback handler and handle rejected clipboard promises: before
attaching the fallback handler call
jQuery(document).off('click.wu-copy-fallback') (similar to how .wu-copy is
cleaned) and when calling navigator.clipboard.writeText in both the ClipboardJS
error callback and the fallback click handler, append .catch(...) to handle
rejections (e.g., log via console.warn or call showCopyFeedback fallback) to
avoid unhandled promise rejections; ensure you still call showCopyFeedback on
success and keep cleanup of window._wu_clipboard_instance as-is so duplicate
handlers are prevented.
| window.wu_initialize_clipboardjs = function() { | ||
|
|
||
| new ClipboardJS('.wu-copy'); | ||
| // Prevent page jump on copy link click | ||
| jQuery(document).off('click.wu-copy').on('click.wu-copy', 'a.wu-copy[href="#"]', function(e) { | ||
| e.preventDefault(); | ||
| }); | ||
|
|
||
| // Destroy previous instance to avoid duplicate handlers on repeated calls | ||
| if (window._wu_clipboard_instance) { | ||
| window._wu_clipboard_instance.destroy(); | ||
| } | ||
|
|
||
| function showCopyFeedback(trigger) { | ||
| const $trigger = jQuery(trigger); | ||
| const $textNodes = $trigger.contents().filter(function() { | ||
| return this.nodeType === 3 && this.textContent.trim().length > 0; | ||
| }); | ||
|
|
||
| if ($textNodes.length) { | ||
| const node = $textNodes[ 0 ]; | ||
| const originalText = node.textContent; | ||
| node.textContent = ' Copied!'; | ||
| setTimeout(function() { | ||
| node.textContent = originalText; | ||
| }, 2000); | ||
| } | ||
| } | ||
|
|
||
| if (typeof ClipboardJS !== 'undefined') { | ||
| window._wu_clipboard_instance = new ClipboardJS('.wu-copy'); | ||
|
|
||
| window._wu_clipboard_instance.on('success', function(e) { | ||
| showCopyFeedback(e.trigger); | ||
| e.clearSelection(); | ||
| }); | ||
|
|
||
| window._wu_clipboard_instance.on('error', function(e) { | ||
| const text = e.trigger.getAttribute('data-clipboard-text'); | ||
| if (text && navigator.clipboard && navigator.clipboard.writeText) { | ||
| navigator.clipboard.writeText(text).then(function() { | ||
| showCopyFeedback(e.trigger); | ||
| }); | ||
| } | ||
| }); | ||
| } else { | ||
| // Fallback when ClipboardJS is not available | ||
| jQuery(document).off('click.wu-copy-fallback').on('click.wu-copy-fallback', '.wu-copy', function() { | ||
| const text = jQuery(this).attr('data-clipboard-text'); | ||
| const trigger = this; | ||
| if (text && navigator.clipboard && navigator.clipboard.writeText) { | ||
| navigator.clipboard.writeText(text).then(function() { | ||
| showCopyFeedback(trigger); | ||
| }); | ||
| } | ||
| }); | ||
| } |
There was a problem hiding this comment.
Handle clipboard fallback cleanup and promise rejection.
If init runs before ClipboardJS loads, the old .wu-copy-fallback handler can stay attached and cause duplicate copy attempts later. Also, navigator.clipboard.writeText rejections are unhandled (e.g., insecure context), which can surface as unhandled promise rejections.
🔧 Suggested fix
if (typeof ClipboardJS !== 'undefined') {
+ jQuery(document).off('click.wu-copy-fallback');
window._wu_clipboard_instance = new ClipboardJS('.wu-copy');
window._wu_clipboard_instance.on('success', function(e) {
showCopyFeedback(e.trigger);
e.clearSelection();
});
window._wu_clipboard_instance.on('error', function(e) {
const text = e.trigger.getAttribute('data-clipboard-text');
if (text && navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(text).then(function() {
- showCopyFeedback(e.trigger);
- });
+ navigator.clipboard.writeText(text)
+ .then(function() {
+ showCopyFeedback(e.trigger);
+ })
+ .catch(function() {
+ // Optionally surface failure feedback here
+ });
}
});
} else {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| window.wu_initialize_clipboardjs = function() { | |
| new ClipboardJS('.wu-copy'); | |
| // Prevent page jump on copy link click | |
| jQuery(document).off('click.wu-copy').on('click.wu-copy', 'a.wu-copy[href="#"]', function(e) { | |
| e.preventDefault(); | |
| }); | |
| // Destroy previous instance to avoid duplicate handlers on repeated calls | |
| if (window._wu_clipboard_instance) { | |
| window._wu_clipboard_instance.destroy(); | |
| } | |
| function showCopyFeedback(trigger) { | |
| const $trigger = jQuery(trigger); | |
| const $textNodes = $trigger.contents().filter(function() { | |
| return this.nodeType === 3 && this.textContent.trim().length > 0; | |
| }); | |
| if ($textNodes.length) { | |
| const node = $textNodes[ 0 ]; | |
| const originalText = node.textContent; | |
| node.textContent = ' Copied!'; | |
| setTimeout(function() { | |
| node.textContent = originalText; | |
| }, 2000); | |
| } | |
| } | |
| if (typeof ClipboardJS !== 'undefined') { | |
| window._wu_clipboard_instance = new ClipboardJS('.wu-copy'); | |
| window._wu_clipboard_instance.on('success', function(e) { | |
| showCopyFeedback(e.trigger); | |
| e.clearSelection(); | |
| }); | |
| window._wu_clipboard_instance.on('error', function(e) { | |
| const text = e.trigger.getAttribute('data-clipboard-text'); | |
| if (text && navigator.clipboard && navigator.clipboard.writeText) { | |
| navigator.clipboard.writeText(text).then(function() { | |
| showCopyFeedback(e.trigger); | |
| }); | |
| } | |
| }); | |
| } else { | |
| // Fallback when ClipboardJS is not available | |
| jQuery(document).off('click.wu-copy-fallback').on('click.wu-copy-fallback', '.wu-copy', function() { | |
| const text = jQuery(this).attr('data-clipboard-text'); | |
| const trigger = this; | |
| if (text && navigator.clipboard && navigator.clipboard.writeText) { | |
| navigator.clipboard.writeText(text).then(function() { | |
| showCopyFeedback(trigger); | |
| }); | |
| } | |
| }); | |
| } | |
| window.wu_initialize_clipboardjs = function() { | |
| // Prevent page jump on copy link click | |
| jQuery(document).off('click.wu-copy').on('click.wu-copy', 'a.wu-copy[href="#"]', function(e) { | |
| e.preventDefault(); | |
| }); | |
| // Destroy previous instance to avoid duplicate handlers on repeated calls | |
| if (window._wu_clipboard_instance) { | |
| window._wu_clipboard_instance.destroy(); | |
| } | |
| function showCopyFeedback(trigger) { | |
| const $trigger = jQuery(trigger); | |
| const $textNodes = $trigger.contents().filter(function() { | |
| return this.nodeType === 3 && this.textContent.trim().length > 0; | |
| }); | |
| if ($textNodes.length) { | |
| const node = $textNodes[ 0 ]; | |
| const originalText = node.textContent; | |
| node.textContent = ' Copied!'; | |
| setTimeout(function() { | |
| node.textContent = originalText; | |
| }, 2000); | |
| } | |
| } | |
| if (typeof ClipboardJS !== 'undefined') { | |
| jQuery(document).off('click.wu-copy-fallback'); | |
| window._wu_clipboard_instance = new ClipboardJS('.wu-copy'); | |
| window._wu_clipboard_instance.on('success', function(e) { | |
| showCopyFeedback(e.trigger); | |
| e.clearSelection(); | |
| }); | |
| window._wu_clipboard_instance.on('error', function(e) { | |
| const text = e.trigger.getAttribute('data-clipboard-text'); | |
| if (text && navigator.clipboard && navigator.clipboard.writeText) { | |
| navigator.clipboard.writeText(text) | |
| .then(function() { | |
| showCopyFeedback(e.trigger); | |
| }) | |
| .catch(function() { | |
| // Optionally surface failure feedback here | |
| }); | |
| } | |
| }); | |
| } else { | |
| // Fallback when ClipboardJS is not available | |
| jQuery(document).off('click.wu-copy-fallback').on('click.wu-copy-fallback', '.wu-copy', function() { | |
| const text = jQuery(this).attr('data-clipboard-text'); | |
| const trigger = this; | |
| if (text && navigator.clipboard && navigator.clipboard.writeText) { | |
| navigator.clipboard.writeText(text).then(function() { | |
| showCopyFeedback(trigger); | |
| }); | |
| } | |
| }); | |
| } |
🤖 Prompt for AI Agents
In `@assets/js/functions.js` around lines 143 - 198, The init function
wu_initialize_clipboardjs must remove any prior fallback handler and handle
rejected clipboard promises: before attaching the fallback handler call
jQuery(document).off('click.wu-copy-fallback') (similar to how .wu-copy is
cleaned) and when calling navigator.clipboard.writeText in both the ClipboardJS
error callback and the fallback click handler, append .catch(...) to handle
rejections (e.g., log via console.warn or call showCopyFeedback fallback) to
avoid unhandled promise rejections; ensure you still call showCopyFeedback on
success and keep cleanup of window._wu_clipboard_instance as-is so duplicate
handlers are prevented.
🔨 Build Complete - Ready for Testing!📦 Download Build Artifact (Recommended)Download the zip build, upload to WordPress and test:
🌐 Test in WordPress Playground (Very Experimental)Click the link below to instantly test this PR in your browser - no installation needed! Login credentials: |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
inc/helpers/class-arr.php (1)
142-154:⚠️ Potential issue | 🟡 MinorAvoid calling
count()inside loop condition.The static analysis tool correctly flags this: calling
count($keys)on every iteration is inefficient. Sincearray_shift()modifies the array each iteration, use a decrementing counter instead.Proposed fix
$keys = explode('.', $key); + $keys_count = count($keys); - while (count($keys) > 1) { + while ($keys_count > 1) { $key = array_shift($keys); + $keys_count--; if ( ! isset($array_to_modify[ $key ]) || ! is_array($array_to_modify[ $key ])) { $array_to_modify[ $key ] = []; } $array_to_modify =& $array_to_modify[ $key ]; }tests/WP_Ultimo/Checkout/Cart_Test.php (1)
658-663:⚠️ Potential issue | 🟡 MinorAvoid hard-coded product IDs in tests (Line 661).
This assumes a product with ID
1exists, which makes the test brittle. Use the helper to create a product and pass its ID.✅ Suggested change
- $cart = new Cart([ - 'cart_type' => 'new', - 'products' => [1], // Assume product exists - 'country' => 'US', - ]); + $plan = $this->create_plan(); + $cart = new Cart([ + 'cart_type' => 'new', + 'products' => [$plan->get_id()], + 'country' => 'US', + ]);
🤖 Fix all issues with AI agents
In `@tests/WP_Ultimo/Helpers/WP_Config_Test.php`:
- Around line 38-44: The test method test_get_wp_config_path_returns_string()
fails when get_wp_config_path() returns null; update the test to handle that by
checking the return value from $this->wp_config->get_wp_config_path() and if it
is null call $this->markTestSkipped(...) with a short message (e.g., "wp-config
not found in test environment"); otherwise proceed with the existing assertions
(assertIsString and assertStringContainsString('.php', ...)). This keeps the
test green in CI when the config file is absent while preserving the original
assertions when a path is returned.
In `@tests/WP_Ultimo/Models/Discount_Code_Test.php`:
- Around line 542-560: The is_one_time() method in class Discount_Code currently
returns the same value as should_apply_to_renewals(), which is inverted: change
Discount_Code::is_one_time() to return the negation of
should_apply_to_renewals() (i.e., ! $this->should_apply_to_renewals()) so a
discount that applies to renewals is not considered one-time, and update the
unit tests test_is_one_time_true() and test_is_one_time_false() in
Discount_Code_Test to assert the opposite expectations (flip the
assertTrue/assertFalse checks to match the corrected behavior).
In `@tests/WP_Ultimo/Models/Email_Test.php`:
- Around line 382-386: The test calls Email::get_by_id($email->get_id()) and
immediately calls $retrieved->get_style(), which will error if retrieval failed;
update the test after the retrieval to assert the retrieval succeeded (e.g.
assertNotFalse($retrieved) or assertInstanceOf(Email::class, $retrieved)) before
invoking get_style(), keeping references to Email::get_by_id, $retrieved,
get_style, save and get_id so it's clear where to add the check.
- Around line 1685-1701: The test test_save_sets_default_draft_status currently
only asserts the saved Email's status is not empty; update it to assert the
actual default value 'draft'. Locate the test method
test_save_sets_default_draft_status in Email_Test.php (uses Email,
set_skip_validation, save and Email::get_by_id) and replace the not-empty
assertion with an equality check that $retrieved->get_status() equals 'draft'
(or the appropriate draft constant if one exists).
- Around line 1437-1442: The tests use assertNotEmpty/assertEmpty for
boolean-returning methods in Email_Test.php; replace those with strict boolean
assertions: use assertTrue($retrieved->is_active()) for is_active(),
assertFalse($retrieved->is_legacy()) for is_legacy(),
assertTrue($retrieved->has_schedule()) for has_schedule(),
assertTrue($retrieved->get_send_copy_to_admin()) for get_send_copy_to_admin(),
and replace assertEquals(true, $retrieved->get_custom_sender()) with
assertTrue($retrieved->get_custom_sender()) to ensure precise boolean checks.
- Around line 1718-1724: The test currently lets get_items_as_array() be empty
and still pass; update the assertions after calling Email::get_items_as_array()
to assert the results are not empty (e.g., assertNotEmpty($results)) and then
verify the saved email is present by checking the first item's keys and values
(for example assertIsArray($results[0]), assertArrayHasKey('title', $results[0])
and assertEquals(expected_title, $results[0]['title']) or assertContains with
the saved title). Locate the block using Email::get_items_as_array() in the test
and add the non-empty and content-specific assertions to ensure the saved email
appears in the returned array.
In `@tests/WP_Ultimo/Models/Membership_Test.php`:
- Around line 1314-1320: The test method test_cancel_already_cancelled assigns
$old_date but never uses it; either remove the unused $old_date assignment or
add an assertion that the cancellation date is unchanged after calling
$this->membership->cancel('Another reason') by comparing $old_date with
$this->membership->get_date_cancellation() (and keep the existing assertSame on
status); target the test_cancel_already_cancelled method and the
membership->get_date_cancellation()/membership->cancel() calls when making the
change.
🧹 Nitpick comments (19)
tests/WP_Ultimo/Helpers/Arr_Test.php (1)
102-109: Consider also asserting the reference modification.Since
Arr::set()takes$array_to_modifyby reference, the test should verify that$arrayitself was modified, not just the return value.Suggested improvement
public function test_set_null_key_replaces_array(): void { $array = ['old' => 'data']; $result = Arr::set($array, null, 'new_value'); $this->assertEquals('new_value', $result); + $this->assertEquals('new_value', $array); }tests/WP_Ultimo/Checkout/Line_Item_Test.php (1)
533-540: Usewp_json_encode()for WordPress compatibility (Line 537).
json_encode()is discouraged in WP contexts and can miss WP-specific flags/filters.✅ Suggested change
- $json = json_encode($line_item); + $json = wp_json_encode($line_item);tests/WP_Ultimo/Checkout/Cart_Test.php (1)
1666-1669: Drop unused$ratein foreach (Line 1668).This variable is unused and flagged by static analysis.
✅ Suggested change
- foreach ($breakthrough as $rate => $total) { + foreach ($breakthrough as $total) { $this->assertEquals(0.0, $total); }tests/WP_Ultimo/Helpers/WP_Config_Test.php (1)
156-188: Strengthen assertions to verify expected line indices.These two tests only assert that the result is an integer, which is a weak assertion. The tests would pass even if the method returned an incorrect line number. Consider asserting the specific expected values.
♻️ Proposed fix to add specific value assertions
public function test_find_reference_hook_line_finds_happy_publishing(): void { $config = [ "<?php\n", "define( 'DB_NAME', 'wordpress' );\n", "/* That's all, stop editing! Happy publishing. */\n", "require_once ABSPATH . 'wp-settings.php';\n", ]; $result = $this->wp_config->find_reference_hook_line($config); - // The Happy Publishing pattern uses -2 offset $this->assertIsInt($result); + // The Happy Publishing comment is at index 2, with -2 offset should return 0 or 1 + // Verify the expected value based on actual method behavior + $this->assertEquals(0, $result); // Adjust expected value as needed } public function test_find_reference_hook_line_finds_php_tag_fallback(): void { $config = [ "<?php\n", "// Some custom config\n", ]; $result = $this->wp_config->find_reference_hook_line($config); $this->assertIsInt($result); + // PHP tag is at index 0, verify expected return value + $this->assertEquals(0, $result); // Adjust expected value as needed }Note: Verify the expected values against the actual
find_reference_hook_lineimplementation to ensure the assertions match the intended behavior.tests/WP_Ultimo/Models/Customer_Test.php (4)
330-339: Incomplete comment and redundant method call.Line 332-333 contains an incomplete comment: "Test that verification key was generated by c" appears truncated. Additionally,
disable_verification_key()is called twice (lines 334 and 337), which is redundant since the first call already disables the key.✏️ Proposed fix
$this->assertGreaterThanOrEqual(Hash::LENGTH, strlen($customer->get_verification_key())); - // Test that verification key was generated by c - // Test disabling verification key + // Test disabling verification key $disable_result = $customer->disable_verification_key(); $this->assertTrue((bool) $disable_result); - // Just check that the key was disabled - $customer->disable_verification_key(); $this->assertEmpty($customer->get_verification_key());
856-865: Boundary test may be timing-sensitive.The test at exactly 3 minutes (
strtotime('-3 minutes')) is sensitive to execution timing. If test execution introduces any delay between settinglast_loginand callingis_online(), the actual elapsed time could exceed the boundary threshold, causing intermittent failures.Consider using a timestamp slightly under the boundary (e.g.,
-2 minutes 59 seconds) or mocking the current time to avoid flakiness in slow CI environments.
1022-1037: Consider documenting the direct property assignment.Line 1030 directly assigns to
$customer->_user, which appears to be an internal property (underscore prefix convention). While this tests an important scenario where user data is injected (likely from a database JOIN), direct property access is fragile and may break if the implementation changes.Consider adding a brief comment explaining why direct property assignment is necessary here, or if the
Customerclass has a public method for this scenario, prefer using that instead.
1646-1656: Consider usingassertFalse()for boolean assertion.Line 1655 uses
assertEmpty($result)to checkhas_trialed(). While this works (sincefalseis considered "empty"), usingassertFalse()would be more explicit about the expected boolean return type and consistent with the companion testtest_set_has_trialed_stores_in_metawhich usesassertTrue().✏️ Suggested change
$result = $customer->has_trialed(); - $this->assertEmpty($result); + $this->assertFalse($result);tests/WP_Ultimo/Models/Membership_Test.php (1)
1337-1339: Consider usingMembership_Status::ACTIVEfor consistency.The rest of the test file uses
Membership_Statusconstants for status values. Using the string'active'here is inconsistent and could mask issues if the constant value ever changes.Proposed fix
- $result = $this->membership->renew(true, 'active'); + $result = $this->membership->renew(true, Membership_Status::ACTIVE); $this->assertTrue($result); - $this->assertSame('active', $this->membership->get_status()); + $this->assertSame(Membership_Status::ACTIVE, $this->membership->get_status());tests/WP_Ultimo/Models/Email_Test.php (3)
222-224: Consider removing the section comment.This comment block adds visual noise to the test file. Test method names should be self-documenting, and IDEs/test runners organize tests by class/method anyway.
236-249: Extract repeated reflection logic into a helper method.The reflection pattern for accessing protected properties is duplicated 20+ times throughout this test file. Consider extracting a helper method to improve maintainability.
♻️ Proposed helper method
/** * Get the value of a protected/private property via reflection. * * `@param` object $object The object instance. * `@param` string $property_name The property name. * `@return` mixed The property value. */ protected function get_protected_property($object, string $property_name) { $reflection = new \ReflectionClass($object); $property = $reflection->getProperty($property_name); if (PHP_VERSION_ID < 80100) { $property->setAccessible(true); } return $property->getValue($object); }Then usages become:
-$reflection = new \ReflectionClass($this->email); -$property = $reflection->getProperty('query_class'); - -if (PHP_VERSION_ID < 80100) { - $property->setAccessible(true); -} - -$this->assertEquals( - \WP_Ultimo\Database\Emails\Email_Query::class, - $property->getValue($this->email), - 'Query class should be Email_Query.' -); +$this->assertEquals( + \WP_Ultimo\Database\Emails\Email_Query::class, + $this->get_protected_property($this->email, 'query_class'), + 'Query class should be Email_Query.' +);
686-701: Consider verifying all provided constructor data.The test data includes
contentbut there's no assertion verifying it was set correctly. For completeness, consider adding:💡 Suggested addition
$this->assertEquals('Constructor Email', $email->get_title()); +$this->assertStringContainsString('Constructor content', strip_tags($email->get_content())); $this->assertEquals('system_email', $email->get_type());tests/WP_Ultimo/Managers/Membership_Manager_Test.php (2)
86-99: Consider usingtearDownAfterClass()or ensure test isolation.The
tearDown()method deletes test entities, but if a test fails mid-execution, orphaned entities may remain. This is acceptable for unit tests, but consider whether these entities might interfere with other tests in the same run.
316-329: Consider adding meaningful assertions beyond smoke tests.Several
transition_membership_status()tests only assert$this->assertTrue(true)after calling the method. While this verifies no exceptions are thrown, consider adding assertions that verify the membership state wasn't unexpectedly modified (e.g., status unchanged when transition should be a no-op).💡 Example enhancement for one test
public function test_transition_membership_status_active_to_cancelled_returns_early(): void { $membership = $this->create_membership(['status' => Membership_Status::ACTIVE]); $manager = $this->get_manager_instance(); + $original_status = $membership->get_status(); // This should return early because 'active' is not in allowed_previous_status. $manager->transition_membership_status( Membership_Status::ACTIVE, Membership_Status::CANCELLED, $membership->get_id() ); - $this->assertTrue(true); + // Verify membership wasn't modified by a no-op transition + $refreshed = wu_get_membership($membership->get_id()); + $this->assertEquals($original_status, $refreshed->get_status()); }Also applies to: 334-346, 351-363, 368-381, 386-399, 404-416, 421-433
tests/WP_Ultimo/Managers/Site_Manager_Test.php (1)
826-840: Consider cleaning up created sites in tests.Multiple tests create sites via
wu_create_site()but don't delete them afterward. While WordPress test framework may handle cleanup between test classes, explicit cleanup ensures test isolation and prevents potential side effects.💡 Example cleanup pattern
public function test_wu_create_site_creates_basic_site(): void { $site = wu_create_site( [ 'title' => 'Basic Test Site', 'domain' => 'basic-test.example.com', 'path' => '/', ] ); $this->assertNotWPError($site); $this->assertInstanceOf(\WP_Ultimo\Models\Site::class, $site); $this->assertNotEmpty($site->get_id()); $this->assertEquals('Basic Test Site', $site->get_title()); + + // Clean up + $site->delete(); }Also applies to: 845-871, 880-893, 898-911
tests/WP_Ultimo/Managers/Domain_Manager_Test.php (1)
1-10: Namespace inconsistency with other test files.This test file uses namespace
WP_Ultimo\Managers, but other test files in this PR (e.g.,Membership_Manager_Test.php,Site_Manager_Test.php) useWP_Ultimo\Tests\Managers. Consider aligning the namespace for consistency.♻️ Proposed fix
<?php -namespace WP_Ultimo\Managers; +namespace WP_Ultimo\Tests\Managers; -use WP_UnitTestCase; +use WP_Ultimo\Managers\Domain_Manager; use WP_Ultimo\Settings; use WP_Ultimo\Models\Domain; use WP_Ultimo\Database\Domains\Domain_Stage; -class Domain_Manager_Test extends WP_UnitTestCase { +class Domain_Manager_Test extends \WP_UnitTestCase {tests/WP_Ultimo/Models/Domain_Test.php (3)
1-15: Namespace inconsistency with other test files.This test file uses namespace
WP_Ultimo\Models, but other test files in this PR useWP_Ultimo\Tests\...namespacing. Consider aligning for consistency.♻️ Proposed fix
<?php -namespace WP_Ultimo\Models; +namespace WP_Ultimo\Tests\Models; -use WP_UnitTestCase; +use WP_Ultimo\Models\Domain; use WP_Ultimo\Database\Domains\Domain_Stage; /** * Test class for Domain model functionality. * * Tests SSL certificate validation for custom domains including * valid certificates, invalid certificates, and empty domain handling. * Also tests getters/setters, business logic, stage methods, and CRUD. */ -class Domain_Test extends WP_UnitTestCase { +class Domain_Test extends \WP_UnitTestCase {
37-68: External network dependency in SSL tests may cause CI flakiness.These tests make real network calls to validate SSL certificates (
dogs.4thelols.ukandeeeeeeeeeeeeeeeeauauexample.com). This can cause intermittent failures due to network issues, DNS resolution, or certificate changes. Consider:
- Mocking the SSL validation for unit tests
- Marking these as integration tests to run separately
- Using
@group networkannotation to skip in CI if needed
17-32: Consider adding tearDown() to clean up created blog.The
setUp()creates a test blog viaself::factory()->blog->create()but there's no correspondingtearDown()to clean it up. While WordPress test framework may handle this, explicit cleanup ensures no side effects.💡 Proposed addition
+ /** + * Clean up test environment. + */ + public function tearDown(): void { + + if ($this->blog_id) { + wp_delete_site($this->blog_id); + } + + parent::tearDown(); + } + /** * Test that has_valid_ssl_certificate returns true for valid SSL certificates. */
| public function test_get_wp_config_path_returns_string(): void { | ||
|
|
||
| $path = $this->wp_config->get_wp_config_path(); | ||
|
|
||
| $this->assertIsString($path); | ||
| $this->assertStringContainsString('.php', $path); | ||
| } |
There was a problem hiding this comment.
Fix failing test: handle null return from get_wp_config_path().
The pipeline shows this test fails because get_wp_config_path() returns null in the test environment (likely because wp-config.php is not found in the expected location). The test should handle this case gracefully.
🐛 Proposed fix to skip test when path is unavailable
public function test_get_wp_config_path_returns_string(): void {
$path = $this->wp_config->get_wp_config_path();
+ if ($path === null) {
+ $this->markTestSkipped('wp-config.php not found in test environment');
+ }
+
$this->assertIsString($path);
$this->assertStringContainsString('.php', $path);
}Alternatively, if get_wp_config_path() should always return a valid path, investigate why it returns null in CI and ensure the test environment is properly configured.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public function test_get_wp_config_path_returns_string(): void { | |
| $path = $this->wp_config->get_wp_config_path(); | |
| $this->assertIsString($path); | |
| $this->assertStringContainsString('.php', $path); | |
| } | |
| public function test_get_wp_config_path_returns_string(): void { | |
| $path = $this->wp_config->get_wp_config_path(); | |
| if ($path === null) { | |
| $this->markTestSkipped('wp-config.php not found in test environment'); | |
| } | |
| $this->assertIsString($path); | |
| $this->assertStringContainsString('.php', $path); | |
| } |
🧰 Tools
🪛 GitHub Actions: Unit & Integration Tests
[error] 42-42: PHPUnit test failed: WP_Config_Test::test_get_wp_config_path_returns_string. Expected WP config path to be a string but was null. Command 'vendor/bin/phpunit' exited with code 1.
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Helpers/WP_Config_Test.php` around lines 38 - 44, The test
method test_get_wp_config_path_returns_string() fails when get_wp_config_path()
returns null; update the test to handle that by checking the return value from
$this->wp_config->get_wp_config_path() and if it is null call
$this->markTestSkipped(...) with a short message (e.g., "wp-config not found in
test environment"); otherwise proceed with the existing assertions
(assertIsString and assertStringContainsString('.php', ...)). This keeps the
test green in CI when the config file is absent while preserving the original
assertions when a path is returned.
| /** | ||
| * Tests is_one_time returns true when apply_to_renewals is true. | ||
| */ | ||
| public function test_is_one_time_true(): void { | ||
| $discount_code = new Discount_Code(); | ||
| $discount_code->set_apply_to_renewals(true); | ||
|
|
||
| $this->assertTrue($discount_code->is_one_time()); | ||
| } | ||
|
|
||
| /** | ||
| * Tests is_one_time returns false when apply_to_renewals is false. | ||
| */ | ||
| public function test_is_one_time_false(): void { | ||
| $discount_code = new Discount_Code(); | ||
| $discount_code->set_apply_to_renewals(false); | ||
|
|
||
| $this->assertFalse($discount_code->is_one_time()); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the Discount_Code class implementation
fd -t php 'Discount_Code' | grep -v Test | head -20Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 251
🏁 Script executed:
# Search for is_one_time method implementation
rg -n 'function is_one_time|public function is_one_time' --type=php -A 3Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 324
🏁 Script executed:
# Search for should_apply_to_renewals method implementation
rg -n 'function should_apply_to_renewals|public function should_apply_to_renewals' --type=php -A 3Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 328
🏁 Script executed:
# Search for set_apply_to_renewals to understand the relationship
rg -n 'function set_apply_to_renewals|public function set_apply_to_renewals' --type=php -A 3Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 363
The is_one_time() method has inverted logic and must be fixed.
The implementation at inc/models/class-discount-code.php:539-542 returns the same value as should_apply_to_renewals():
public function is_one_time() {
return (bool) $this->should_apply_to_renewals();
}This is semantically incorrect. A discount that applies to renewals is NOT one-time. The method should return the inverse:
return ! $this->should_apply_to_renewals();The tests at lines 542-560 currently verify incorrect behavior. Once the source is fixed, these tests will need to be corrected to expect the opposite values.
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Models/Discount_Code_Test.php` around lines 542 - 560, The
is_one_time() method in class Discount_Code currently returns the same value as
should_apply_to_renewals(), which is inverted: change
Discount_Code::is_one_time() to return the negation of
should_apply_to_renewals() (i.e., ! $this->should_apply_to_renewals()) so a
discount that applies to renewals is not considered one-time, and update the
unit tests test_is_one_time_true() and test_is_one_time_false() in
Discount_Code_Test to assert the opposite expectations (flip the
assertTrue/assertFalse checks to match the corrected behavior).
| $email->save(); | ||
|
|
||
| $retrieved = Email::get_by_id($email->get_id()); | ||
| $this->assertEquals('plain-text', $retrieved->get_style(), 'Style should be plain-text after retrieval.'); | ||
| } |
There was a problem hiding this comment.
Add assertion to verify retrieval succeeded before accessing methods.
If save() fails or get_by_id() returns false, the get_style() call on line 385 will cause an error. Add an assertion to verify the object was retrieved.
🛡️ Proposed fix
$email->save();
$retrieved = Email::get_by_id($email->get_id());
+$this->assertInstanceOf(Email::class, $retrieved, 'Email should be retrievable after save.');
$this->assertEquals('plain-text', $retrieved->get_style(), 'Style should be plain-text after retrieval.');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| $email->save(); | |
| $retrieved = Email::get_by_id($email->get_id()); | |
| $this->assertEquals('plain-text', $retrieved->get_style(), 'Style should be plain-text after retrieval.'); | |
| } | |
| $email->save(); | |
| $retrieved = Email::get_by_id($email->get_id()); | |
| $this->assertInstanceOf(Email::class, $retrieved, 'Email should be retrievable after save.'); | |
| $this->assertEquals('plain-text', $retrieved->get_style(), 'Style should be plain-text after retrieval.'); | |
| } |
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Models/Email_Test.php` around lines 382 - 386, The test calls
Email::get_by_id($email->get_id()) and immediately calls
$retrieved->get_style(), which will error if retrieval failed; update the test
after the retrieval to assert the retrieval succeeded (e.g.
assertNotFalse($retrieved) or assertInstanceOf(Email::class, $retrieved)) before
invoking get_style(), keeping references to Email::get_by_id, $retrieved,
get_style, save and get_id so it's clear where to add the check.
| $this->assertNotEmpty($retrieved->is_active(), 'Active status should persist.'); | ||
| $this->assertEmpty($retrieved->is_legacy(), 'Legacy status should persist.'); | ||
| $this->assertNotEmpty($retrieved->has_schedule(), 'Schedule should persist.'); | ||
| $this->assertNotEmpty($retrieved->get_send_copy_to_admin(), 'Send copy to admin should persist.'); | ||
| $this->assertEquals('html', $retrieved->get_style(), 'Style should persist.'); | ||
| $this->assertEquals(true, $retrieved->get_custom_sender(), 'Custom sender should persist.'); |
There was a problem hiding this comment.
Use assertTrue/assertFalse for boolean assertions instead of assertNotEmpty/assertEmpty.
Using assertNotEmpty and assertEmpty for boolean values is imprecise. These assertions will pass for any truthy/falsy value (e.g., 1, "yes", []), potentially masking type-related bugs.
🔧 Proposed fix
-$this->assertNotEmpty($retrieved->is_active(), 'Active status should persist.');
-$this->assertEmpty($retrieved->is_legacy(), 'Legacy status should persist.');
-$this->assertNotEmpty($retrieved->has_schedule(), 'Schedule should persist.');
-$this->assertNotEmpty($retrieved->get_send_copy_to_admin(), 'Send copy to admin should persist.');
+$this->assertTrue($retrieved->is_active(), 'Active status should persist.');
+$this->assertFalse($retrieved->is_legacy(), 'Legacy status should persist.');
+$this->assertTrue($retrieved->has_schedule(), 'Schedule should persist.');
+$this->assertTrue($retrieved->get_send_copy_to_admin(), 'Send copy to admin should persist.');🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Models/Email_Test.php` around lines 1437 - 1442, The tests
use assertNotEmpty/assertEmpty for boolean-returning methods in Email_Test.php;
replace those with strict boolean assertions: use
assertTrue($retrieved->is_active()) for is_active(),
assertFalse($retrieved->is_legacy()) for is_legacy(),
assertTrue($retrieved->has_schedule()) for has_schedule(),
assertTrue($retrieved->get_send_copy_to_admin()) for get_send_copy_to_admin(),
and replace assertEquals(true, $retrieved->get_custom_sender()) with
assertTrue($retrieved->get_custom_sender()) to ensure precise boolean checks.
| * Test save sets default status to draft when status is empty. | ||
| */ | ||
| public function test_save_sets_default_draft_status(): void { | ||
| $email = new Email(); | ||
| $email->set_title('Default Status Test'); | ||
| $email->set_content('Content'); | ||
| $email->set_type('system_email'); | ||
| $email->set_event('test_default'); | ||
| $email->set_slug('default-status'); | ||
| $email->set_target('admin'); | ||
| // Do not set status | ||
| $email->set_skip_validation(true); | ||
| $email->save(); | ||
|
|
||
| $retrieved = Email::get_by_id($email->get_id()); | ||
| $this->assertNotEmpty($retrieved->get_status(), 'Status should have a default value after save.'); | ||
| } |
There was a problem hiding this comment.
Test assertion doesn't match test name - verify the actual default value.
The test is named test_save_sets_default_draft_status but only asserts that status is not empty. It should verify the status is actually 'draft' to match the test's stated purpose.
🔧 Proposed fix
$retrieved = Email::get_by_id($email->get_id());
-$this->assertNotEmpty($retrieved->get_status(), 'Status should have a default value after save.');
+$this->assertEquals('draft', $retrieved->get_status(), 'Status should default to draft when not set.');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| * Test save sets default status to draft when status is empty. | |
| */ | |
| public function test_save_sets_default_draft_status(): void { | |
| $email = new Email(); | |
| $email->set_title('Default Status Test'); | |
| $email->set_content('Content'); | |
| $email->set_type('system_email'); | |
| $email->set_event('test_default'); | |
| $email->set_slug('default-status'); | |
| $email->set_target('admin'); | |
| // Do not set status | |
| $email->set_skip_validation(true); | |
| $email->save(); | |
| $retrieved = Email::get_by_id($email->get_id()); | |
| $this->assertNotEmpty($retrieved->get_status(), 'Status should have a default value after save.'); | |
| } | |
| * Test save sets default status to draft when status is empty. | |
| */ | |
| public function test_save_sets_default_draft_status(): void { | |
| $email = new Email(); | |
| $email->set_title('Default Status Test'); | |
| $email->set_content('Content'); | |
| $email->set_type('system_email'); | |
| $email->set_event('test_default'); | |
| $email->set_slug('default-status'); | |
| $email->set_target('admin'); | |
| // Do not set status | |
| $email->set_skip_validation(true); | |
| $email->save(); | |
| $retrieved = Email::get_by_id($email->get_id()); | |
| $this->assertEquals('draft', $retrieved->get_status(), 'Status should default to draft when not set.'); | |
| } |
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Models/Email_Test.php` around lines 1685 - 1701, The test
test_save_sets_default_draft_status currently only asserts the saved Email's
status is not empty; update it to assert the actual default value 'draft'.
Locate the test method test_save_sets_default_draft_status in Email_Test.php
(uses Email, set_skip_validation, save and Email::get_by_id) and replace the
not-empty assertion with an equality check that $retrieved->get_status() equals
'draft' (or the appropriate draft constant if one exists).
| $results = Email::get_items_as_array(); | ||
| $this->assertIsArray($results, 'get_items_as_array should return an array.'); | ||
| if ( ! empty($results)) { | ||
| $this->assertIsArray($results[0], 'Each item should be an array.'); | ||
| $this->assertArrayHasKey('title', $results[0], 'Item arrays should have title key.'); | ||
| } | ||
| } |
There was a problem hiding this comment.
Strengthen assertion to ensure saved email is included in results.
The conditional assertion allows the test to pass even if get_items_as_array() returns an empty array after saving an email, which would indicate a bug.
🔧 Proposed fix
$results = Email::get_items_as_array();
$this->assertIsArray($results, 'get_items_as_array should return an array.');
-if ( ! empty($results)) {
- $this->assertIsArray($results[0], 'Each item should be an array.');
- $this->assertArrayHasKey('title', $results[0], 'Item arrays should have title key.');
-}
+$this->assertNotEmpty($results, 'get_items_as_array should contain the saved email.');
+$this->assertIsArray($results[0], 'Each item should be an array.');
+$this->assertArrayHasKey('title', $results[0], 'Item arrays should have title key.');🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Models/Email_Test.php` around lines 1718 - 1724, The test
currently lets get_items_as_array() be empty and still pass; update the
assertions after calling Email::get_items_as_array() to assert the results are
not empty (e.g., assertNotEmpty($results)) and then verify the saved email is
present by checking the first item's keys and values (for example
assertIsArray($results[0]), assertArrayHasKey('title', $results[0]) and
assertEquals(expected_title, $results[0]['title']) or assertContains with the
saved title). Locate the block using Email::get_items_as_array() in the test and
add the non-empty and content-specific assertions to ensure the saved email
appears in the returned array.
| public function test_cancel_already_cancelled(): void { | ||
| $this->membership->set_status(Membership_Status::CANCELLED); | ||
| $old_date = $this->membership->get_date_cancellation(); | ||
| $this->membership->cancel('Another reason'); | ||
| // Status should still be cancelled, date should not change. | ||
| $this->assertSame(Membership_Status::CANCELLED, $this->membership->get_status()); | ||
| } |
There was a problem hiding this comment.
Remove unused variable or add assertion.
The variable $old_date is assigned but never used. If the intent is to verify that the cancellation date doesn't change when cancelling an already-cancelled membership, add an assertion.
Proposed fix: either remove the variable or add an assertion
Option 1: Remove unused variable
public function test_cancel_already_cancelled(): void {
$this->membership->set_status(Membership_Status::CANCELLED);
- $old_date = $this->membership->get_date_cancellation();
$this->membership->cancel('Another reason');
// Status should still be cancelled, date should not change.
$this->assertSame(Membership_Status::CANCELLED, $this->membership->get_status());
}Option 2: Add assertion to verify date doesn't change
public function test_cancel_already_cancelled(): void {
$this->membership->set_status(Membership_Status::CANCELLED);
+ $this->membership->set_date_cancellation(gmdate('Y-m-d H:i:s', strtotime('-1 day')));
$old_date = $this->membership->get_date_cancellation();
$this->membership->cancel('Another reason');
- // Status should still be cancelled, date should not change.
$this->assertSame(Membership_Status::CANCELLED, $this->membership->get_status());
+ $this->assertSame($old_date, $this->membership->get_date_cancellation(), 'Cancellation date should not change for already cancelled membership.');
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public function test_cancel_already_cancelled(): void { | |
| $this->membership->set_status(Membership_Status::CANCELLED); | |
| $old_date = $this->membership->get_date_cancellation(); | |
| $this->membership->cancel('Another reason'); | |
| // Status should still be cancelled, date should not change. | |
| $this->assertSame(Membership_Status::CANCELLED, $this->membership->get_status()); | |
| } | |
| public function test_cancel_already_cancelled(): void { | |
| $this->membership->set_status(Membership_Status::CANCELLED); | |
| $this->membership->cancel('Another reason'); | |
| // Status should still be cancelled, date should not change. | |
| $this->assertSame(Membership_Status::CANCELLED, $this->membership->get_status()); | |
| } |
🧰 Tools
🪛 PHPMD (2.15.0)
[warning] 1316-1316: Avoid unused local variables such as '$old_date'. (undefined)
(UnusedLocalVariable)
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Models/Membership_Test.php` around lines 1314 - 1320, The
test method test_cancel_already_cancelled assigns $old_date but never uses it;
either remove the unused $old_date assignment or add an assertion that the
cancellation date is unchanged after calling $this->membership->cancel('Another
reason') by comparing $old_date with $this->membership->get_date_cancellation()
(and keep the existing assertSame on status); target the
test_cancel_already_cancelled method and the
membership->get_date_cancellation()/membership->cancel() calls when making the
change.
Use WP_TESTS_DIR env var with sys_get_temp_dir fallback instead of hardcoding /tmp/wordpress-tests-lib path. Also add a fallback return to satisfy PHPStan's return type check. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🔨 Build Complete - Ready for Testing!📦 Download Build Artifact (Recommended)Download the zip build, upload to WordPress and test:
🌐 Test in WordPress Playground (Very Experimental)Click the link below to instantly test this PR in your browser - no installation needed! Login credentials: |
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
inc/helpers/class-arr.php (1)
142-152:⚠️ Potential issue | 🟠 MajorAvoid
count()in the loop condition to unblock CI.Code Quality Checks are failing on Line 144 due to
count()in the loop condition. Cache the count and decrement it as you shift keys to preserve the same behavior and satisfy the rule.🔧 Proposed fix
- $keys = explode('.', $key); - - while (count($keys) > 1) { + $keys = explode('.', $key); + $keys_count = count($keys); + + while ($keys_count > 1) { $key = array_shift($keys); + $keys_count--;
🤖 Fix all issues with AI agents
In `@tests/WP_Ultimo/Checkout/Cart_Test.php`:
- Around line 1341-1363: The test mutates the global setting 'force_auto_renew'
and currently resets it to a hard-coded true, which can leak state; update
test_auto_renew_false to read and save the original value (e.g. $orig =
wu_get_setting('force_auto_renew') or equivalent) before calling
wu_save_setting(false), and restore that original value at the end (use
wu_save_setting($orig)); to be robust, perform the restore in a finally block
(try { ... assertions ... } finally { wu_save_setting($orig); }) so
Cart::should_auto_renew() and the global setting are always returned to their
prior state.
- Around line 1642-1693: The foreach in
test_tax_breakthrough_empty_when_no_taxes declares an unused $rate causing
PHPMD; update the loop in that test (which asserts totals from
Cart::get_tax_breakthrough()) to iterate values only (e.g., iterate over
$breakthrough’s values or use a throwaway variable name) so only $total is
declared and asserted, removing the unused $rate variable.
In `@tests/WP_Ultimo/Managers/Domain_Manager_Test.php`:
- Around line 1210-1218: The test method
test_handle_site_created_without_subdomain in Domain_Manager_Test declares an
unused global $current_site; remove that declaration to eliminate the
unused-variable warning. Edit the test_handle_site_created_without_subdomain
method and delete the line "global $current_site" so the test still calls
$this->domain_manager->handle_site_created($site) and asserts true without
referencing the global.
- Around line 1840-1844: The anonymous function attached to the add_action hook
'wu_async_remove_old_primary_domains' declares an unused parameter $domains;
remove the unused parameter from the closure signature (i.e., change function
($domains) use (&$action_called) to function () use (&$action_called)) or, if
you actually need to inspect passed domains, reference $domains inside the
closure (e.g., check its contents) — update the closure attached to add_action
so the signature matches its usage while keeping $action_called as the flag.
- Around line 1918-1938: The test method test_dns_get_record_filter_is_applied
sets a local flag $filter_called in the closure passed to add_filter but never
asserts it, so replace the final assertTrue(true) with an assertion that
$filter_called is true; keep the try-catch around
Domain_Manager::dns_get_record('example.com') and ensure
remove_all_filters('wu_domain_dns_get_record') still runs (use finally or call
it after the try/catch), and remove the unused $domain parameter from the
closure or reference it to avoid unused-variable warnings.
- Around line 879-947: Move the repeated global $wpdb->wu_dmtable initialization
into the test class setUp() so it runs before each test: add the global $wpdb
check and assignment (if empty, set to $wpdb->base_prefix .
'wu_domain_mappings') inside the existing setUp() (where
Domain_Manager::get_instance() is already called), and remove the duplicated
global $wpdb checks from test_get_by_domain, test_get_by_domain_nonexistent, and
test_get_by_domain_with_array so those tests rely on the centralized setUp()
initialization.
In `@tests/WP_Ultimo/Models/Membership_Test.php`:
- Around line 773-831: The tests call wp_set_current_user() with the Ultimo
customer ID (from $this->customer->get_id()) which is the wrong identifier;
update the two wp_set_current_user(...) calls to pass the corresponding
WordPress user ID instead (retrieve it from the customer fixture, e.g.
$wp_user_id = $this->customer->get_user_id() or
$this->customer->get_wp_user_id() as appropriate) and in the non-matching case
use a different WP user ID (not the Ultimo customer ID) so that
wp_set_current_user(...) receives a WP user ID while still asserting
membership->is_customer_allowed(...) behavior.
🧹 Nitpick comments (14)
tests/WP_Ultimo/Checkout/Line_Item_Test.php (2)
125-138: Consider assertingget_tax_total()for tax-inclusive scenario.The test verifies that the total remains unchanged with tax-inclusive pricing, but doesn't assert the computed tax amount. Adding an assertion for
get_tax_total()would strengthen this test by confirming the tax is correctly extracted from the inclusive price.💡 Suggested improvement
$this->assertEquals(100, $line_item->get_subtotal()); // Tax inclusive means total stays the same $this->assertEquals(100, $line_item->get_total()); + // Tax amount should be extracted from the inclusive price (~9.09 for 10% on 100) + $this->assertGreaterThan(0, $line_item->get_tax_total());
533-541: Usewp_json_encode()instead ofjson_encode()for WordPress compatibility.In WordPress codebases,
wp_json_encode()is preferred as it handles encoding errors more gracefully and ensures proper UTF-8 encoding.♻️ Proposed fix
public function test_json_encode(): void { $line_item = $this->create_line_item(['title' => 'Premium Plan']); - $json = json_encode($line_item); + $json = wp_json_encode($line_item); $this->assertIsString($json); $this->assertStringContainsString('Premium Plan', $json); }tests/WP_Ultimo/Helpers/WP_Config_Test.php (1)
159-172: Consider asserting specific expected values for deterministic test configs.The "Happy Publishing" test only asserts the result is an integer without verifying the actual value. Since the test config is deterministic, you could assert the specific expected line number for stronger validation:
// Happy Publishing at index 2 with -2 offset = 0 $this->assertEquals(0, $result);Same applies to
test_find_reference_hook_line_finds_php_tag_fallback()at line 186.tests/WP_Ultimo/Managers/Site_Manager_Test.php (4)
492-509: Unused closure parameters$fromand$to.The closure declares
$fromand$toparameters but doesn't use them. While this tests that the filter is called with 3 arguments, you can suppress the warning by prefixing unused params with underscore.✨ Suggested fix
- add_filter('wu_search_and_replace_on_duplication', function ($settings, $from, $to) { + add_filter('wu_search_and_replace_on_duplication', function ($settings, $_from, $_to) { $settings['filter-key'] = 'filter-value'; return $settings; }, 10, 3);
916-927: UsemarkTestSkipped()instead of conditionalassertTrue(true).The pattern of using
assertTrue(true, 'message')in an else branch to "skip" a test is unconventional and doesn't actually mark the test as skipped in PHPUnit's reporting. UsingmarkTestSkipped()provides clearer test output and intent.✨ Suggested fix
// On the main site, get_type() should return 'main' if ($site->get_id() === $main_site_id) { $this->assertEquals('main', $site->get_type()); } else { - $this->assertTrue(true, 'Current site is not main; skipping.'); + $this->markTestSkipped('Current site is not main; skipping.'); }The same pattern appears at lines 1042-1044 in
test_additional_thumbnail_sizes_on_main_site().
1075-1088: Test assertion is not verifying actual behavior.This test only asserts
assertTrue(true, ...)which doesn't verify any actual behavior beyond "no exception was thrown." Consider asserting that the meta box was not registered when the setting is disabled, mirroring the enabled test's assertion pattern.✨ Suggested fix
public function test_add_no_index_warning_disabled(): void { wu_save_setting('stop_template_indexing', false); $manager = $this->get_manager_instance(); $manager->add_no_index_warning(); - // Should not have added the meta box. - // We can check there is no error/exception. - $this->assertTrue(true, 'No exception thrown when stop_template_indexing is false.'); + // Check the meta box was NOT registered. + global $wp_meta_boxes; + + $found = isset($wp_meta_boxes['dashboard-network']['normal']['high']['wu-warnings']); + + $this->assertFalse($found, 'Meta box wu-warnings should NOT be registered when stop_template_indexing is false.'); }
1112-1123: Test assertion could verify more specific behavior.Similar to the previous comment, asserting
assertTrue(true, ...)only confirms no exception. Consider verifying that the method returns early (e.g., returnsnullor a specific value) or that no side effects occurred for a non-existing site.tests/WP_Ultimo/Managers/Membership_Manager_Test.php (3)
316-433: Consider strengthening assertions for transition tests.Several tests in this section only assert
$this->assertTrue(true)after callingtransition_membership_status(). This pattern only verifies the method doesn't throw an exception, not that it behaves correctly.For "returns early" tests, consider verifying no side effects occurred (e.g., no pending sites published, no hooks triggered). For valid transitions, verify expected state changes if applicable.
Example improvement for early-return test
public function test_transition_membership_status_active_to_cancelled_returns_early(): void { $membership = $this->create_membership(['status' => Membership_Status::ACTIVE]); $manager = $this->get_manager_instance(); + $action_fired = false; + add_action('wu_membership_post_activation', function() use (&$action_fired) { + $action_fired = true; + }); + // This should return early because 'active' is not in allowed_previous_status. $manager->transition_membership_status( Membership_Status::ACTIVE, Membership_Status::CANCELLED, $membership->get_id() ); - $this->assertTrue(true); + // Verify no activation actions were triggered (early return) + $this->assertFalse($action_fired, 'Expected early return, but activation action fired'); }
656-665: Prefix unused closure parameters with underscore.The static analysis correctly identifies that
$old_statusand$new_statusare unused. Following PHP convention, prefix them with underscore to signal intentional non-use.Alternatively, these parameters could be used to strengthen the test by verifying the correct status values are passed.
Option 1: Prefix unused parameters
add_action( 'wu_transition_membership_status', - function ($old_status, $new_status, $id) use (&$fired, $membership) { + function ($_old_status, $_new_status, $id) use (&$fired, $membership) { if ($id === $membership->get_id()) { $fired = true; } },Option 2: Use parameters to strengthen assertion
+$captured_old = null; +$captured_new = null; + add_action( 'wu_transition_membership_status', - function ($old_status, $new_status, $id) use (&$fired, $membership) { + function ($old_status, $new_status, $id) use (&$fired, &$captured_old, &$captured_new, $membership) { if ($id === $membership->get_id()) { $fired = true; + $captured_old = $old_status; + $captured_new = $new_status; } }, 1, 3 ); $membership->set_status(Membership_Status::ACTIVE); $membership->save(); $this->assertTrue($fired); +$this->assertEquals(Membership_Status::PENDING, $captured_old); +$this->assertEquals(Membership_Status::ACTIVE, $captured_new);
513-564: Consider ensuring cleanup on test failure.In
test_async_transfer_membership_successandtest_async_transfer_membership_invalid_membership_id, the$new_customer->delete()call at the end won't execute if an assertion fails earlier. If WP_UnitTestCase doesn't rollback the entire transaction, this could leave orphaned test data.Use try/finally to guarantee cleanup
public function test_async_transfer_membership_success(): void { $membership = $this->create_membership(); $manager = $this->get_manager_instance(); $new_customer = wu_create_customer( [ 'username' => 'transfer_target_' . wp_rand(), 'email' => 'transfer_target_' . wp_rand() . '@example.com', 'password' => 'password123', ] ); if (is_wp_error($new_customer)) { $this->fail('Could not create target customer: ' . $new_customer->get_error_message()); } - $manager->async_transfer_membership($membership->get_id(), $new_customer->get_id()); - - $refreshed = wu_get_membership($membership->get_id()); - - $this->assertEquals($new_customer->get_id(), $refreshed->get_customer_id()); - - $new_customer->delete(); + try { + $manager->async_transfer_membership($membership->get_id(), $new_customer->get_id()); + + $refreshed = wu_get_membership($membership->get_id()); + + $this->assertEquals($new_customer->get_id(), $refreshed->get_customer_id()); + } finally { + $new_customer->delete(); + } }tests/WP_Ultimo/Models/Email_Test.php (1)
236-293: Consider extracting reflection helper to reduce duplication.The pattern for accessing protected/private properties via reflection is repeated approximately 20 times throughout the test file. Consider extracting this to a helper method for improved maintainability.
♻️ Suggested helper method
/** * Get the value of a protected/private property via reflection. * * `@param` object $object The object instance. * `@param` string $property The property name. * `@return` mixed The property value. */ protected function get_protected_property($object, string $property) { $reflection = new \ReflectionClass($object); $prop = $reflection->getProperty($property); if (PHP_VERSION_ID < 80100) { $prop->setAccessible(true); } return $prop->getValue($object); }Example usage:
-$reflection = new \ReflectionClass($this->email); -$property = $reflection->getProperty('meta'); - -if (PHP_VERSION_ID < 80100) { - $property->setAccessible(true); -} - -$meta = $property->getValue($this->email); +$meta = $this->get_protected_property($this->email, 'meta');Also applies to: 335-476, 497-598, 1583-1597
tests/WP_Ultimo/Models/Domain_Test.php (2)
37-68: Consider mocking SSL validation to avoid external network dependencies.These tests make real network calls to external domains (
dogs.4thelols.uk,eeeeeeeeeeeeeeeeauauexample.com). This creates several issues:
- Tests will fail if domains become unreachable or change SSL status
- Tests are slow due to network latency
- Tests require network access, breaking CI environments with restricted connectivity
Consider mocking the SSL validation logic or using a test double pattern to make these tests deterministic and isolated.
894-906: Caching test doesn't verify actual caching behavior.This test only verifies that calling
get_path()twice returns the same value, which would pass even without any caching mechanism. To properly test caching, you would need to verify that the underlying lookup (e.g., database query orget_blog_details()) is only called once.💡 Example approach using a counter or mock
public function test_get_path_is_cached(): void { $domain = new Domain(); $domain->set_blog_id($this->blog_id); // Use a filter or mock to track calls to the underlying method $call_count = 0; add_filter('get_blog_details', function($details) use (&$call_count) { $call_count++; return $details; }, 10, 1); $path1 = $domain->get_path(); $path2 = $domain->get_path(); $this->assertSame($path1, $path2); $this->assertEquals(1, $call_count, 'get_blog_details should only be called once due to caching'); remove_all_filters('get_blog_details'); }tests/WP_Ultimo/Managers/Domain_Manager_Test.php (1)
538-635: Consider extracting shared Domain model tests to reduce duplication.There's significant overlap between these Domain model property tests and those in
Domain_Test.php. Tests foris_active(),has_inactive_stage(), stage getters/setters, and stage labels/classes are duplicated across both files.Consider consolidating model-specific tests in
Domain_Test.phpand keeping only manager-specific integration tests inDomain_Manager_Test.php.Also applies to: 1420-1546
| /** | ||
| * Test should_auto_renew defaults to true. | ||
| */ | ||
| public function test_auto_renew_default_true() { | ||
| $cart = new Cart([]); | ||
|
|
||
| $this->assertTrue($cart->should_auto_renew()); | ||
| } | ||
|
|
||
| /** | ||
| * Test auto_renew can be set to false. | ||
| */ | ||
| public function test_auto_renew_false() { | ||
| // The auto_renew setting is only applied when force_auto_renew is disabled. | ||
| wu_save_setting('force_auto_renew', false); | ||
|
|
||
| $cart = new Cart(['auto_renew' => false]); | ||
|
|
||
| $this->assertFalse($cart->should_auto_renew()); | ||
|
|
||
| // Clean up setting | ||
| wu_save_setting('force_auto_renew', true); | ||
| } |
There was a problem hiding this comment.
Restore the original force_auto_renew setting after the test.
This test hard-sets the setting to true at the end, which can leak state if the original value was different.
🔧 Suggested fix
// The auto_renew setting is only applied when force_auto_renew is disabled.
- wu_save_setting('force_auto_renew', false);
+ $original_force = wu_get_setting('force_auto_renew', true);
+ wu_save_setting('force_auto_renew', false);
@@
- // Clean up setting
- wu_save_setting('force_auto_renew', true);
+ // Clean up setting
+ wu_save_setting('force_auto_renew', $original_force);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Test should_auto_renew defaults to true. | |
| */ | |
| public function test_auto_renew_default_true() { | |
| $cart = new Cart([]); | |
| $this->assertTrue($cart->should_auto_renew()); | |
| } | |
| /** | |
| * Test auto_renew can be set to false. | |
| */ | |
| public function test_auto_renew_false() { | |
| // The auto_renew setting is only applied when force_auto_renew is disabled. | |
| wu_save_setting('force_auto_renew', false); | |
| $cart = new Cart(['auto_renew' => false]); | |
| $this->assertFalse($cart->should_auto_renew()); | |
| // Clean up setting | |
| wu_save_setting('force_auto_renew', true); | |
| } | |
| /** | |
| * Test should_auto_renew defaults to true. | |
| */ | |
| public function test_auto_renew_default_true() { | |
| $cart = new Cart([]); | |
| $this->assertTrue($cart->should_auto_renew()); | |
| } | |
| /** | |
| * Test auto_renew can be set to false. | |
| */ | |
| public function test_auto_renew_false() { | |
| // The auto_renew setting is only applied when force_auto_renew is disabled. | |
| $original_force = wu_get_setting('force_auto_renew', true); | |
| wu_save_setting('force_auto_renew', false); | |
| $cart = new Cart(['auto_renew' => false]); | |
| $this->assertFalse($cart->should_auto_renew()); | |
| // Clean up setting | |
| wu_save_setting('force_auto_renew', $original_force); | |
| } |
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Checkout/Cart_Test.php` around lines 1341 - 1363, The test
mutates the global setting 'force_auto_renew' and currently resets it to a
hard-coded true, which can leak state; update test_auto_renew_false to read and
save the original value (e.g. $orig = wu_get_setting('force_auto_renew') or
equivalent) before calling wu_save_setting(false), and restore that original
value at the end (use wu_save_setting($orig)); to be robust, perform the restore
in a finally block (try { ... assertions ... } finally { wu_save_setting($orig);
}) so Cart::should_auto_renew() and the global setting are always returned to
their prior state.
| /** | ||
| * Test get_total_taxes is zero when taxes are not enabled. | ||
| */ | ||
| public function test_total_taxes_zero_when_taxes_disabled() { | ||
| $plan = $this->create_plan(['amount' => 100.00]); | ||
|
|
||
| $cart = new Cart([ | ||
| 'products' => [$plan->get_id()], | ||
| 'country' => 'US', | ||
| ]); | ||
|
|
||
| $this->assertEquals(0.0, $cart->get_total_taxes()); | ||
| } | ||
|
|
||
| /** | ||
| * Test tax breakthrough is empty when taxes are disabled. | ||
| */ | ||
| public function test_tax_breakthrough_empty_when_no_taxes() { | ||
| $plan = $this->create_plan(['amount' => 100.00]); | ||
|
|
||
| $cart = new Cart([ | ||
| 'products' => [$plan->get_id()], | ||
| ]); | ||
|
|
||
| $breakthrough = $cart->get_tax_breakthrough(); | ||
| // All tax rates should be 0 when taxes are disabled | ||
| foreach ($breakthrough as $rate => $total) { | ||
| $this->assertEquals(0.0, $total); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Test is_tax_exempt returns false by default. | ||
| */ | ||
| public function test_is_tax_exempt_default() { | ||
| $cart = new Cart([]); | ||
|
|
||
| $this->assertFalse($cart->is_tax_exempt()); | ||
| } | ||
|
|
||
| /** | ||
| * Test is_tax_exempt can be filtered to true. | ||
| */ | ||
| public function test_is_tax_exempt_filtered() { | ||
| add_filter('wu_cart_is_tax_exempt', '__return_true'); | ||
|
|
||
| $cart = new Cart([]); | ||
|
|
||
| $this->assertTrue($cart->is_tax_exempt()); | ||
|
|
||
| remove_filter('wu_cart_is_tax_exempt', '__return_true'); | ||
| } |
There was a problem hiding this comment.
Remove the unused $rate variable to satisfy PHPMD.
$rate isn’t used in the loop body, which triggers the static analysis warning.
🛠️ Suggested fix
- foreach ($breakthrough as $rate => $total) {
+ foreach ($breakthrough as $total) {
$this->assertEquals(0.0, $total);
}🧰 Tools
🪛 PHPMD (2.15.0)
[warning] 1668-1668: Avoid unused local variables such as '$rate'. (undefined)
(UnusedLocalVariable)
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Checkout/Cart_Test.php` around lines 1642 - 1693, The foreach
in test_tax_breakthrough_empty_when_no_taxes declares an unused $rate causing
PHPMD; update the loop in that test (which asserts totals from
Cart::get_tax_breakthrough()) to iterate values only (e.g., iterate over
$breakthrough’s values or use a throwaway variable name) so only $total is
declared and asserted, removing the unused $rate variable.
| public function test_get_by_domain(): void { | ||
| global $wpdb; | ||
|
|
||
| // Ensure wu_dmtable is set | ||
| if (empty($wpdb->wu_dmtable)) { | ||
| $wpdb->wu_dmtable = $wpdb->base_prefix . 'wu_domain_mappings'; | ||
| } | ||
|
|
||
| $domain = wu_create_domain([ | ||
| 'blog_id' => $this->get_blog_id(), | ||
| 'domain' => 'get-by-domain-test.example.com', | ||
| 'stage' => Domain_Stage::DONE, | ||
| ]); | ||
|
|
||
| $this->assertNotWPError($domain); | ||
|
|
||
| wp_cache_flush(); | ||
|
|
||
| $fetched = Domain::get_by_domain('get-by-domain-test.example.com'); | ||
|
|
||
| $this->assertInstanceOf(Domain::class, $fetched); | ||
| $this->assertEquals('get-by-domain-test.example.com', $fetched->get_domain()); | ||
| } | ||
|
|
||
| /** | ||
| * Test Domain::get_by_domain returns null for non-existent domain. | ||
| */ | ||
| public function test_get_by_domain_nonexistent(): void { | ||
| global $wpdb; | ||
|
|
||
| if (empty($wpdb->wu_dmtable)) { | ||
| $wpdb->wu_dmtable = $wpdb->base_prefix . 'wu_domain_mappings'; | ||
| } | ||
|
|
||
| wp_cache_flush(); | ||
|
|
||
| $fetched = Domain::get_by_domain('nonexistent-domain-xyz.example.com'); | ||
|
|
||
| $this->assertNull($fetched); | ||
| } | ||
|
|
||
| /** | ||
| * Test Domain::get_by_domain with array of domains. | ||
| */ | ||
| public function test_get_by_domain_with_array(): void { | ||
| global $wpdb; | ||
|
|
||
| if (empty($wpdb->wu_dmtable)) { | ||
| $wpdb->wu_dmtable = $wpdb->base_prefix . 'wu_domain_mappings'; | ||
| } | ||
|
|
||
| $domain = wu_create_domain([ | ||
| 'blog_id' => $this->get_blog_id(), | ||
| 'domain' => 'array-test-domain.example.com', | ||
| 'stage' => Domain_Stage::DONE, | ||
| ]); | ||
|
|
||
| $this->assertNotWPError($domain); | ||
|
|
||
| wp_cache_flush(); | ||
|
|
||
| $fetched = Domain::get_by_domain([ | ||
| 'nonexistent.example.com', | ||
| 'array-test-domain.example.com', | ||
| ]); | ||
|
|
||
| $this->assertInstanceOf(Domain::class, $fetched); | ||
| $this->assertEquals('array-test-domain.example.com', $fetched->get_domain()); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check where wu_dmtable is set in the codebase (excluding tests)
rg -n 'wu_dmtable\s*=' --type=php -g '!tests/*' | head -30Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 201
🏁 Script executed:
#!/bin/bash
# Also check the Domain_Manager_Test class for setUp and initialization methods
rg -n 'function setUp|function tearDown|wu_dmtable' tests/WP_Ultimo/Managers/Domain_Manager_Test.php | head -40Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 474
🏁 Script executed:
#!/bin/bash
# Check if there's a bootstrap file or base test class
fd -e php -path '*tests*' -name '*bootstrap*' -o -name '*TestCase*' -o -name '*base*' | grep -i test | head -20Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 249
🏁 Script executed:
#!/bin/bash
# Look at the beginning of Domain_Manager_Test.php to understand the class structure
head -100 tests/WP_Ultimo/Managers/Domain_Manager_Test.phpRepository: Ultimate-Multisite/ultimate-multisite
Length of output: 3273
🏁 Script executed:
#!/bin/bash
# Check context around line 119 in inc/class-domain-mapping.php
sed -n '100,140p' inc/class-domain-mapping.phpRepository: Ultimate-Multisite/ultimate-multisite
Length of output: 1095
🏁 Script executed:
#!/bin/bash
# Search for wu_create_domain function to see if it initializes wu_dmtable
rg -n 'function wu_create_domain' --type=php -A 20 | head -40Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 1171
🏁 Script executed:
#!/bin/bash
# Check if other Domain-related tests use the same wu_dmtable setup pattern
rg -n 'wu_dmtable' tests/ --type=php -B 2 | head -50Repository: Ultimate-Multisite/ultimate-multisite
Length of output: 1156
Move wu_dmtable initialization to setUp() to eliminate duplication.
The same initialization code appears in all three test_get_by_domain* methods (lines 882-884, 909-911, 926-928). Since setUp() is called before each test, move this to the existing setUp() method instead:
public function setUp(): void {
parent::setUp();
$this->domain_manager = Domain_Manager::get_instance();
global $wpdb;
if (empty($wpdb->wu_dmtable)) {
$wpdb->wu_dmtable = $wpdb->base_prefix . 'wu_domain_mappings';
}
}This will eliminate the repetitive checks and ensure the table is properly initialized for all tests that need it.
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Managers/Domain_Manager_Test.php` around lines 879 - 947,
Move the repeated global $wpdb->wu_dmtable initialization into the test class
setUp() so it runs before each test: add the global $wpdb check and assignment
(if empty, set to $wpdb->base_prefix . 'wu_domain_mappings') inside the existing
setUp() (where Domain_Manager::get_instance() is already called), and remove the
duplicated global $wpdb checks from test_get_by_domain,
test_get_by_domain_nonexistent, and test_get_by_domain_with_array so those tests
rely on the centralized setUp() initialization.
| public function test_handle_site_created_without_subdomain(): void { | ||
| global $current_site; | ||
|
|
||
| // Use the main site directly | ||
| $site = get_blog_details(1); | ||
|
|
||
| // Should return early - no domain record creation | ||
| $this->domain_manager->handle_site_created($site); | ||
| $this->assertTrue(true); |
There was a problem hiding this comment.
Remove unused global $current_site declaration.
The $current_site global is declared but never used in this test method. Static analysis correctly flags this as an unused variable.
🧹 Proposed fix
public function test_handle_site_created_without_subdomain(): void {
- global $current_site;
-
// Use the main site directly
$site = get_blog_details(1);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public function test_handle_site_created_without_subdomain(): void { | |
| global $current_site; | |
| // Use the main site directly | |
| $site = get_blog_details(1); | |
| // Should return early - no domain record creation | |
| $this->domain_manager->handle_site_created($site); | |
| $this->assertTrue(true); | |
| public function test_handle_site_created_without_subdomain(): void { | |
| // Use the main site directly | |
| $site = get_blog_details(1); | |
| // Should return early - no domain record creation | |
| $this->domain_manager->handle_site_created($site); | |
| $this->assertTrue(true); |
🧰 Tools
🪛 PHPMD (2.15.0)
[warning] 1211-1211: Avoid unused local variables such as '$current_site'. (undefined)
(UnusedLocalVariable)
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Managers/Domain_Manager_Test.php` around lines 1210 - 1218,
The test method test_handle_site_created_without_subdomain in
Domain_Manager_Test declares an unused global $current_site; remove that
declaration to eliminate the unused-variable warning. Edit the
test_handle_site_created_without_subdomain method and delete the line "global
$current_site" so the test still calls
$this->domain_manager->handle_site_created($site) and asserts true without
referencing the global.
| // when domain2 is created as primary | ||
| $action_called = false; | ||
| add_action('wu_async_remove_old_primary_domains', function ($domains) use (&$action_called) { | ||
| $action_called = true; | ||
| }); |
There was a problem hiding this comment.
Unused $domains parameter in closure.
The $domains parameter is captured but never used. Either use it or remove it from the closure signature.
🧹 Proposed fix
- add_action('wu_async_remove_old_primary_domains', function ($domains) use (&$action_called) {
+ add_action('wu_async_remove_old_primary_domains', function () use (&$action_called) {
$action_called = true;
});🧰 Tools
🪛 PHPMD (2.15.0)
[warning] 1842-1842: Avoid unused parameters such as '$domains'. (undefined)
(UnusedFormalParameter)
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Managers/Domain_Manager_Test.php` around lines 1840 - 1844,
The anonymous function attached to the add_action hook
'wu_async_remove_old_primary_domains' declares an unused parameter $domains;
remove the unused parameter from the closure signature (i.e., change function
($domains) use (&$action_called) to function () use (&$action_called)) or, if
you actually need to inspect passed domains, reference $domains inside the
closure (e.g., check its contents) — update the closure attached to add_action
so the signature matches its usage while keeping $action_called as the flag.
| public function test_dns_get_record_filter_is_applied(): void { | ||
| $filter_called = false; | ||
|
|
||
| add_filter('wu_domain_dns_get_record', function ($results, $domain) use (&$filter_called) { | ||
| $filter_called = true; | ||
| return $results; | ||
| }, 10, 2); | ||
|
|
||
| // This might throw an exception due to DNS resolvers not being available | ||
| // in the test environment, so we wrap in a try-catch | ||
| try { | ||
| Domain_Manager::dns_get_record('example.com'); | ||
| } catch (\Throwable $e) { | ||
| // DNS resolution may fail in test env, that's fine | ||
| } | ||
|
|
||
| // The filter may or may not have been called depending on | ||
| // whether dns_get_record completed before throwing | ||
| // This test mainly validates the filter exists | ||
| remove_all_filters('wu_domain_dns_get_record'); | ||
| $this->assertTrue(true); |
There was a problem hiding this comment.
DNS record filter test doesn't assert filter was called.
The test sets $filter_called = true inside the filter callback but never asserts this value. The test always passes with $this->assertTrue(true) regardless of whether the filter was invoked. Additionally, the $domain parameter in the filter closure is unused.
🧹 Proposed fix to properly assert filter invocation
public function test_dns_get_record_filter_is_applied(): void {
$filter_called = false;
- add_filter('wu_domain_dns_get_record', function ($results, $domain) use (&$filter_called) {
+ add_filter('wu_domain_dns_get_record', function ($results) use (&$filter_called) {
$filter_called = true;
return $results;
- }, 10, 2);
+ }, 10, 1);
// This might throw an exception due to DNS resolvers not being available
// in the test environment, so we wrap in a try-catch
try {
Domain_Manager::dns_get_record('example.com');
} catch (\Throwable $e) {
// DNS resolution may fail in test env, that's fine
}
- // The filter may or may not have been called depending on
- // whether dns_get_record completed before throwing
- // This test mainly validates the filter exists
remove_all_filters('wu_domain_dns_get_record');
- $this->assertTrue(true);
+
+ // If DNS resolution completed (even with failure), the filter should have been called
+ // If it threw before reaching the filter, skip this assertion
+ if ($filter_called) {
+ $this->assertTrue($filter_called, 'wu_domain_dns_get_record filter should be applied');
+ } else {
+ $this->markTestSkipped('DNS resolution failed before filter could be applied');
+ }
}🧰 Tools
🪛 PHPMD (2.15.0)
[warning] 1919-1919: Avoid unused local variables such as '$filter_called'. (undefined)
(UnusedLocalVariable)
[warning] 1921-1921: Avoid unused parameters such as '$domain'. (undefined)
(UnusedFormalParameter)
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Managers/Domain_Manager_Test.php` around lines 1918 - 1938,
The test method test_dns_get_record_filter_is_applied sets a local flag
$filter_called in the closure passed to add_filter but never asserts it, so
replace the final assertTrue(true) with an assertion that $filter_called is
true; keep the try-catch around Domain_Manager::dns_get_record('example.com')
and ensure remove_all_filters('wu_domain_dns_get_record') still runs (use
finally or call it after the try/catch), and remove the unused $domain parameter
from the closure or reference it to avoid unused-variable warnings.
| /** | ||
| * Test get_customer_id and set_customer_id. | ||
| */ | ||
| public function test_get_and_set_customer_id(): void { | ||
| $this->membership->set_customer_id(999); | ||
| $this->assertSame(999, $this->membership->get_customer_id()); | ||
| } | ||
|
|
||
| $faker->generate_fake_memberships(); | ||
| /** | ||
| * Test get_customer returns a customer object. | ||
| */ | ||
| public function test_get_customer(): void { | ||
| $customer = $this->membership->get_customer(); | ||
| $this->assertInstanceOf(Customer::class, $customer); | ||
| $this->assertEquals($this->customer->get_id(), $customer->get_id()); | ||
| } | ||
|
|
||
| $this->membership = current($faker->get_fake_data_generated('memberships')); | ||
| // Create a new Membership instance for each test. | ||
| // $this->membership = new Membership(); | ||
| /** | ||
| * Test get_user_id and set_user_id. | ||
| */ | ||
| public function test_get_and_set_user_id(): void { | ||
| $this->membership->set_user_id(42); | ||
| $this->assertSame(42, $this->membership->get_user_id()); | ||
| } | ||
|
|
||
| // Set a default customer ID and plan ID. | ||
| // $this->membership->set_customer_id($this->customer->get_id()); | ||
| // $this->membership->set_plan_id($this->product->get_id()); | ||
| /** | ||
| * Test get_plan_id and set_plan_id. | ||
| */ | ||
| public function test_get_and_set_plan_id(): void { | ||
| $this->membership->set_plan_id(123); | ||
| $this->assertSame(123, $this->membership->get_plan_id()); | ||
| } | ||
|
|
||
| /** | ||
| * Test if the customer is allowed access to the membership. | ||
| * Test get_plan returns a product object. | ||
| */ | ||
| public function test_is_customer_allowed(): void { | ||
| // Admins with 'manage_network' capability should always return true. | ||
| $admin_user_id = $this->factory()->user->create(['role' => 'administrator']); | ||
| grant_super_admin($admin_user_id); | ||
| wp_set_current_user($admin_user_id); | ||
| $this->assertTrue($this->membership->is_customer_allowed(), 'Failed asserting that admin is allowed.'); | ||
| public function test_get_plan(): void { | ||
| $plan = $this->membership->get_plan(); | ||
| $this->assertInstanceOf(Product::class, $plan); | ||
| $this->assertEquals($this->product->get_id(), $plan->get_id()); | ||
| } | ||
|
|
||
| // Regular customers are allowed if IDs match. | ||
| /** | ||
| * Test is_customer_allowed with matching customer. | ||
| */ | ||
| public function test_is_customer_allowed_matching(): void { | ||
| $customer_id = $this->customer->get_id(); | ||
| wp_set_current_user($customer_id); | ||
| $this->assertTrue( | ||
| $this->membership->is_customer_allowed($customer_id), | ||
| 'Failed asserting that customer with matching ID is allowed.' | ||
| ); | ||
| $this->assertTrue($this->membership->is_customer_allowed($customer_id)); | ||
| } | ||
|
|
||
| // Regular customers are denied if IDs do not match. | ||
| /** | ||
| * Test is_customer_allowed with non-matching customer. | ||
| */ | ||
| public function test_is_customer_allowed_non_matching(): void { | ||
| $wrong_customer_id = 456; | ||
| wp_set_current_user($wrong_customer_id); | ||
| $this->assertFalse( | ||
| $this->membership->is_customer_allowed($wrong_customer_id), | ||
| 'Failed asserting that customer with non-matching ID is denied.' | ||
| ); | ||
| $this->assertFalse($this->membership->is_customer_allowed($wrong_customer_id)); | ||
| } |
There was a problem hiding this comment.
Use the WP user ID when setting current user.
wp_set_current_user() expects a WordPress user ID; passing the Ultimo customer ID can set the wrong user and make these tests inaccurate or flaky.
🔧 Suggested fix
- $customer_id = $this->customer->get_id();
- wp_set_current_user($customer_id);
+ $customer_id = $this->customer->get_id();
+ wp_set_current_user($this->customer->get_user_id());
@@
- $wrong_customer_id = 456;
- wp_set_current_user($wrong_customer_id);
+ $wrong_customer_id = 456;
+ $other_user_id = self::factory()->user->create();
+ wp_set_current_user($other_user_id);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Test get_customer_id and set_customer_id. | |
| */ | |
| public function test_get_and_set_customer_id(): void { | |
| $this->membership->set_customer_id(999); | |
| $this->assertSame(999, $this->membership->get_customer_id()); | |
| } | |
| $faker->generate_fake_memberships(); | |
| /** | |
| * Test get_customer returns a customer object. | |
| */ | |
| public function test_get_customer(): void { | |
| $customer = $this->membership->get_customer(); | |
| $this->assertInstanceOf(Customer::class, $customer); | |
| $this->assertEquals($this->customer->get_id(), $customer->get_id()); | |
| } | |
| $this->membership = current($faker->get_fake_data_generated('memberships')); | |
| // Create a new Membership instance for each test. | |
| // $this->membership = new Membership(); | |
| /** | |
| * Test get_user_id and set_user_id. | |
| */ | |
| public function test_get_and_set_user_id(): void { | |
| $this->membership->set_user_id(42); | |
| $this->assertSame(42, $this->membership->get_user_id()); | |
| } | |
| // Set a default customer ID and plan ID. | |
| // $this->membership->set_customer_id($this->customer->get_id()); | |
| // $this->membership->set_plan_id($this->product->get_id()); | |
| /** | |
| * Test get_plan_id and set_plan_id. | |
| */ | |
| public function test_get_and_set_plan_id(): void { | |
| $this->membership->set_plan_id(123); | |
| $this->assertSame(123, $this->membership->get_plan_id()); | |
| } | |
| /** | |
| * Test if the customer is allowed access to the membership. | |
| * Test get_plan returns a product object. | |
| */ | |
| public function test_is_customer_allowed(): void { | |
| // Admins with 'manage_network' capability should always return true. | |
| $admin_user_id = $this->factory()->user->create(['role' => 'administrator']); | |
| grant_super_admin($admin_user_id); | |
| wp_set_current_user($admin_user_id); | |
| $this->assertTrue($this->membership->is_customer_allowed(), 'Failed asserting that admin is allowed.'); | |
| public function test_get_plan(): void { | |
| $plan = $this->membership->get_plan(); | |
| $this->assertInstanceOf(Product::class, $plan); | |
| $this->assertEquals($this->product->get_id(), $plan->get_id()); | |
| } | |
| // Regular customers are allowed if IDs match. | |
| /** | |
| * Test is_customer_allowed with matching customer. | |
| */ | |
| public function test_is_customer_allowed_matching(): void { | |
| $customer_id = $this->customer->get_id(); | |
| wp_set_current_user($customer_id); | |
| $this->assertTrue( | |
| $this->membership->is_customer_allowed($customer_id), | |
| 'Failed asserting that customer with matching ID is allowed.' | |
| ); | |
| $this->assertTrue($this->membership->is_customer_allowed($customer_id)); | |
| } | |
| // Regular customers are denied if IDs do not match. | |
| /** | |
| * Test is_customer_allowed with non-matching customer. | |
| */ | |
| public function test_is_customer_allowed_non_matching(): void { | |
| $wrong_customer_id = 456; | |
| wp_set_current_user($wrong_customer_id); | |
| $this->assertFalse( | |
| $this->membership->is_customer_allowed($wrong_customer_id), | |
| 'Failed asserting that customer with non-matching ID is denied.' | |
| ); | |
| $this->assertFalse($this->membership->is_customer_allowed($wrong_customer_id)); | |
| } | |
| /** | |
| * Test get_customer_id and set_customer_id. | |
| */ | |
| public function test_get_and_set_customer_id(): void { | |
| $this->membership->set_customer_id(999); | |
| $this->assertSame(999, $this->membership->get_customer_id()); | |
| } | |
| /** | |
| * Test get_customer returns a customer object. | |
| */ | |
| public function test_get_customer(): void { | |
| $customer = $this->membership->get_customer(); | |
| $this->assertInstanceOf(Customer::class, $customer); | |
| $this->assertEquals($this->customer->get_id(), $customer->get_id()); | |
| } | |
| /** | |
| * Test get_user_id and set_user_id. | |
| */ | |
| public function test_get_and_set_user_id(): void { | |
| $this->membership->set_user_id(42); | |
| $this->assertSame(42, $this->membership->get_user_id()); | |
| } | |
| /** | |
| * Test get_plan_id and set_plan_id. | |
| */ | |
| public function test_get_and_set_plan_id(): void { | |
| $this->membership->set_plan_id(123); | |
| $this->assertSame(123, $this->membership->get_plan_id()); | |
| } | |
| /** | |
| * Test get_plan returns a product object. | |
| */ | |
| public function test_get_plan(): void { | |
| $plan = $this->membership->get_plan(); | |
| $this->assertInstanceOf(Product::class, $plan); | |
| $this->assertEquals($this->product->get_id(), $plan->get_id()); | |
| } | |
| /** | |
| * Test is_customer_allowed with matching customer. | |
| */ | |
| public function test_is_customer_allowed_matching(): void { | |
| $customer_id = $this->customer->get_id(); | |
| wp_set_current_user($this->customer->get_user_id()); | |
| $this->assertTrue($this->membership->is_customer_allowed($customer_id)); | |
| } | |
| /** | |
| * Test is_customer_allowed with non-matching customer. | |
| */ | |
| public function test_is_customer_allowed_non_matching(): void { | |
| $wrong_customer_id = 456; | |
| $other_user_id = self::factory()->user->create(); | |
| wp_set_current_user($other_user_id); | |
| $this->assertFalse($this->membership->is_customer_allowed($wrong_customer_id)); | |
| } |
🤖 Prompt for AI Agents
In `@tests/WP_Ultimo/Models/Membership_Test.php` around lines 773 - 831, The tests
call wp_set_current_user() with the Ultimo customer ID (from
$this->customer->get_id()) which is the wrong identifier; update the two
wp_set_current_user(...) calls to pass the corresponding WordPress user ID
instead (retrieve it from the customer fixture, e.g. $wp_user_id =
$this->customer->get_user_id() or $this->customer->get_wp_user_id() as
appropriate) and in the non-matching case use a different WP user ID (not the
Ultimo customer ID) so that wp_set_current_user(...) receives a WP user ID while
still asserting membership->is_customer_allowed(...) behavior.
Summary
is_admin() || wp_doing_ajax(), saving ~100-150ms on every frontend and cron requestinittoadmin_initso it no longer fires on frontend/cronTest plan
🤖 Generated with Claude Code
Summary by CodeRabbit